feat(lessons): add lessons from client db
This commit is contained in:
871
src/pages/student/lessons/AreaVolumeLesson.tsx
Normal file
871
src/pages/student/lessons/AreaVolumeLesson.tsx
Normal file
@ -0,0 +1,871 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Square,
|
||||
Circle,
|
||||
Box,
|
||||
Layers,
|
||||
Ruler,
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import CompositeAreaWidget from "../../../components/lessons/CompositeAreaWidget";
|
||||
import InteractiveSectorWidget from "../../../components/lessons/InteractiveSectorWidget";
|
||||
import CompositeSolidsWidget from "../../../components/lessons/CompositeSolidsWidget";
|
||||
import ScaleFactorWidget from "../../../components/lessons/ScaleFactorWidget";
|
||||
import { AREA_VOL_EASY, AREA_VOL_MEDIUM } from "../../../data/math/area-volume";
|
||||
|
||||
/* ─── Clickable formula card with shape diagram + example ─── */
|
||||
function FormulaCard({
|
||||
formula,
|
||||
diagram,
|
||||
example,
|
||||
}: {
|
||||
formula: React.ReactNode;
|
||||
diagram: React.ReactNode;
|
||||
example: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={`w-full text-left glass-formula rounded-xl border transition-all duration-300 overflow-hidden ${open ? "border-emerald-300 shadow-md" : "border-emerald-100 hover:border-emerald-200"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between py-3 px-5">
|
||||
<span className="font-mono text-base font-bold text-slate-800">
|
||||
{formula}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-emerald-400 shrink-0 transition-transform duration-300 ${open ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 ease-in-out ${open ? "max-h-[400px] opacity-100" : "max-h-0 opacity-0"}`}
|
||||
>
|
||||
<div className="border-t border-emerald-100 px-5 py-4 flex flex-col sm:flex-row items-center gap-5 bg-gradient-to-br from-emerald-50/50 to-white/80">
|
||||
<div className="shrink-0">{diagram}</div>
|
||||
<div className="text-sm text-slate-600 space-y-1 font-mono">
|
||||
{example}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Shape SVG diagrams ─── */
|
||||
const RectSvg = () => (
|
||||
<svg width="140" height="95" viewBox="-5 -5 130 90" overflow="visible">
|
||||
<rect
|
||||
x="10"
|
||||
y="10"
|
||||
width="100"
|
||||
height="60"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
rx="2"
|
||||
/>
|
||||
<text
|
||||
x="60"
|
||||
y="82"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
l = 8
|
||||
</text>
|
||||
<text
|
||||
x="2"
|
||||
y="44"
|
||||
textAnchor="end"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
transform="rotate(-90 2 44)"
|
||||
>
|
||||
w = 5
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const TriSvg = () => (
|
||||
<svg width="140" height="105" viewBox="-5 -5 130 105" overflow="visible">
|
||||
<polygon
|
||||
points="60,10 10,80 110,80"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="60"
|
||||
y1="10"
|
||||
x2="60"
|
||||
y2="80"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<text
|
||||
x="60"
|
||||
y="96"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
b = 10
|
||||
</text>
|
||||
<text x="68" y="50" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 6
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const ParaSvg = () => (
|
||||
<svg width="150" height="95" viewBox="-5 -5 140 90" overflow="visible">
|
||||
<polygon
|
||||
points="30,10 120,10 100,70 10,70"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="30"
|
||||
y1="10"
|
||||
x2="30"
|
||||
y2="70"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<text
|
||||
x="60"
|
||||
y="84"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
b = 9
|
||||
</text>
|
||||
<text x="20" y="44" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 4
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const TrapSvg = () => (
|
||||
<svg width="150" height="105" viewBox="-5 -5 140 105" overflow="visible">
|
||||
<polygon
|
||||
points="35,18 95,18 115,75 15,75"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="35"
|
||||
y1="18"
|
||||
x2="35"
|
||||
y2="75"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<text
|
||||
x="65"
|
||||
y="14"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
b₁ = 6
|
||||
</text>
|
||||
<text
|
||||
x="65"
|
||||
y="92"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
b₂ = 10
|
||||
</text>
|
||||
<text x="25" y="52" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 4
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const CircAreaSvg = () => (
|
||||
<svg width="120" height="110" viewBox="-5 -5 110 110" overflow="visible">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line x1="50" y1="50" x2="90" y2="50" stroke="#059669" strokeWidth="2" />
|
||||
<circle cx="50" cy="50" r="2.5" fill="#059669" />
|
||||
<text x="70" y="44" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 5
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const CircCircSvg = () => (
|
||||
<svg width="120" height="110" viewBox="-5 -5 110 110" overflow="visible">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="none"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line x1="10" y1="50" x2="90" y2="50" stroke="#059669" strokeWidth="2" />
|
||||
<text
|
||||
x="50"
|
||||
y="44"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
d = 10
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const PrismSASvg = () => (
|
||||
<svg width="140" height="115" viewBox="-5 -5 130 115" overflow="visible">
|
||||
<polygon
|
||||
points="20,40 70,40 70,90 20,90"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<polygon
|
||||
points="20,40 45,20 95,20 70,40"
|
||||
fill="#a7f3d0"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<polygon
|
||||
points="70,40 95,20 95,70 70,90"
|
||||
fill="#6ee7b7"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
x="45"
|
||||
y="105"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
l = 4
|
||||
</text>
|
||||
<text x="10" y="68" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 2
|
||||
</text>
|
||||
<text x="88" y="48" className="text-[13px] font-bold fill-emerald-700">
|
||||
w = 3
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const CylSASvg = () => (
|
||||
<svg width="120" height="120" viewBox="-5 -5 115 120" overflow="visible">
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="25"
|
||||
rx="35"
|
||||
ry="12"
|
||||
fill="#a7f3d0"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="15" y="25" width="70" height="60" fill="#d1fae5" />
|
||||
<line x1="15" y1="25" x2="15" y2="85" stroke="#059669" strokeWidth="2" />
|
||||
<line x1="85" y1="25" x2="85" y2="85" stroke="#059669" strokeWidth="2" />
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="85"
|
||||
rx="35"
|
||||
ry="12"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="50"
|
||||
y1="25"
|
||||
x2="85"
|
||||
y2="25"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="3"
|
||||
/>
|
||||
<text x="67" y="19" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 3
|
||||
</text>
|
||||
<text x="92" y="58" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 5
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const SphereSvg = () => (
|
||||
<svg width="120" height="110" viewBox="-5 -5 110 110" overflow="visible">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="50"
|
||||
rx="40"
|
||||
ry="14"
|
||||
fill="none"
|
||||
stroke="#059669"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<line x1="50" y1="50" x2="90" y2="50" stroke="#059669" strokeWidth="2" />
|
||||
<circle cx="50" cy="50" r="2.5" fill="#059669" />
|
||||
<text x="70" y="44" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 4
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const PrismVolSvg = () => (
|
||||
<svg width="140" height="115" viewBox="-5 -5 130 115" overflow="visible">
|
||||
<polygon
|
||||
points="20,40 70,40 70,90 20,90"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<polygon
|
||||
points="20,40 45,20 95,20 70,40"
|
||||
fill="#a7f3d0"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<polygon
|
||||
points="70,40 95,20 95,70 70,90"
|
||||
fill="#6ee7b7"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
x="45"
|
||||
y="105"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
l = 6
|
||||
</text>
|
||||
<text x="10" y="68" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 4
|
||||
</text>
|
||||
<text x="88" y="48" className="text-[13px] font-bold fill-emerald-700">
|
||||
w = 3
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const CylVolSvg = () => (
|
||||
<svg width="120" height="120" viewBox="-5 -5 115 120" overflow="visible">
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="25"
|
||||
rx="35"
|
||||
ry="12"
|
||||
fill="#a7f3d0"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="15" y="25" width="70" height="60" fill="#d1fae5" />
|
||||
<line x1="15" y1="25" x2="15" y2="85" stroke="#059669" strokeWidth="2" />
|
||||
<line x1="85" y1="25" x2="85" y2="85" stroke="#059669" strokeWidth="2" />
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="85"
|
||||
rx="35"
|
||||
ry="12"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text x="67" y="19" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 5
|
||||
</text>
|
||||
<text x="92" y="58" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 8
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const ConeSvg = () => (
|
||||
<svg width="120" height="120" viewBox="-5 -5 115 120" overflow="visible">
|
||||
<polygon
|
||||
points="50,10 15,90 85,90"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="90"
|
||||
rx="35"
|
||||
ry="12"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="50"
|
||||
y1="10"
|
||||
x2="50"
|
||||
y2="90"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<text x="55" y="56" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 9
|
||||
</text>
|
||||
<text x="67" y="96" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 4
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const SphereVolSvg = () => (
|
||||
<svg width="120" height="110" viewBox="-5 -5 110 110" overflow="visible">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="50"
|
||||
rx="40"
|
||||
ry="14"
|
||||
fill="none"
|
||||
stroke="#059669"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<line x1="50" y1="50" x2="90" y2="50" stroke="#059669" strokeWidth="2" />
|
||||
<circle cx="50" cy="50" r="2.5" fill="#059669" />
|
||||
<text x="70" y="44" className="text-[13px] font-bold fill-emerald-700">
|
||||
r = 6
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
const PyramidSvg = () => (
|
||||
<svg width="130" height="125" viewBox="-5 -5 120 125" overflow="visible">
|
||||
<polygon
|
||||
points="55,10 10,90 100,90"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<polygon
|
||||
points="55,10 100,90 80,100 35,100 10,90"
|
||||
fill="#d1fae5"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<line
|
||||
x1="55"
|
||||
y1="10"
|
||||
x2="55"
|
||||
y2="90"
|
||||
stroke="#059669"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="4"
|
||||
/>
|
||||
<text x="62" y="56" className="text-[13px] font-bold fill-emerald-600">
|
||||
h = 12
|
||||
</text>
|
||||
<text
|
||||
x="55"
|
||||
y="114"
|
||||
textAnchor="middle"
|
||||
className="text-[13px] font-bold fill-emerald-700"
|
||||
>
|
||||
B = 25
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Area Formulas", icon: Square },
|
||||
{ title: "Composite Shapes", icon: Layers },
|
||||
{ title: "Arc Length & Sector Area", icon: Circle },
|
||||
{ title: "Surface Area", icon: Box },
|
||||
{ title: "Volume", icon: Ruler },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function AreaVolumeLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Area & Volume"
|
||||
sections={SECTIONS}
|
||||
color="emerald"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Area Formulas */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Area Formulas
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
All area formulas are on the SAT reference sheet, but knowing them
|
||||
saves time. <strong>Tap any formula</strong> to see a diagram and
|
||||
worked example.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<FormulaCard
|
||||
formula="Rectangle: A = l × w"
|
||||
diagram={<RectSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>l = 8, w = 5</p>
|
||||
<p>
|
||||
A = 8 × 5 = <strong className="text-emerald-700">40</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula={
|
||||
<>
|
||||
Triangle: A = <Frac n="1" d="2" /> × b × h
|
||||
</>
|
||||
}
|
||||
diagram={<TriSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>b = 10, h = 6</p>
|
||||
<p>
|
||||
A = ½ × 10 × 6 ={" "}
|
||||
<strong className="text-emerald-700">30</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Parallelogram: A = b × h"
|
||||
diagram={<ParaSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>b = 9, h = 4</p>
|
||||
<p>
|
||||
A = 9 × 4 = <strong className="text-emerald-700">36</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula={
|
||||
<>
|
||||
Trapezoid: A = <Frac n="1" d="2" />
|
||||
(b₁ + b₂) × h
|
||||
</>
|
||||
}
|
||||
diagram={<TrapSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>b₁ = 6, b₂ = 10, h = 4</p>
|
||||
<p>
|
||||
A = ½(6+10) × 4 ={" "}
|
||||
<strong className="text-emerald-700">32</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Circle: A = πr²"
|
||||
diagram={<CircAreaSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 5</p>
|
||||
<p>
|
||||
A = π(25) ={" "}
|
||||
<strong className="text-emerald-700">25π ≈ 78.5</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Circumference: C = 2πr = πd"
|
||||
diagram={<CircCircSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>d = 10 (r = 5)</p>
|
||||
<p>
|
||||
C = 2π(5) ={" "}
|
||||
<strong className="text-emerald-700">10π ≈ 31.4</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
The height of a triangle is ALWAYS perpendicular to the base —
|
||||
it's NOT the side length (unless it's a right triangle).
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Composite Shapes */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Composite Shapes
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Break complex shapes into simpler pieces. <strong>Add</strong> areas
|
||||
for combined shapes. <strong>Subtract</strong> for cut-out regions.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: L-Shape" color="emerald">
|
||||
<p>10 × 8 rectangle with a 4 × 3 piece cut out</p>
|
||||
<p className="text-slate-500">
|
||||
80 − 12 ={" "}
|
||||
<strong className="text-emerald-700">68 square units</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Shaded Region" color="emerald">
|
||||
<p>Square with side 10 and inscribed circle (r = 5)</p>
|
||||
<p className="text-slate-500">
|
||||
Shaded area = 100 − 25π ≈{" "}
|
||||
<strong className="text-emerald-700">21.5 square units</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<CompositeAreaWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Arc Length & Sector Area */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Arc Length & Sector Area
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A sector is a "pizza slice" of a circle. The fraction of the circle
|
||||
used = central angle ÷ 360°.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>Arc Length = (θ ÷ 360) × 2πr</FormulaBox>
|
||||
<FormulaBox>Sector Area = (θ ÷ 360) × πr²</FormulaBox>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Circle with r = 6, central angle = 60°</p>
|
||||
<p className="text-slate-500">
|
||||
Arc = (60 ÷ 360) × 2π(6) = <Frac n="1" d="6" /> × 12π = 2π
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Sector = (60 ÷ 360) × π(36) ={" "}
|
||||
<strong className="text-emerald-700">6π</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<InteractiveSectorWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Surface Area */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Surface Area
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Surface area is the total area of all faces or surfaces of a 3D
|
||||
shape. <strong>Tap any formula</strong> to see the shape and a
|
||||
worked example.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaCard
|
||||
formula="Rectangular Prism: SA = 2(lw + lh + wh)"
|
||||
diagram={<PrismSASvg />}
|
||||
example={
|
||||
<>
|
||||
<p>l = 4, w = 3, h = 2</p>
|
||||
<p>SA = 2(12 + 8 + 6)</p>
|
||||
<p>
|
||||
SA = 2(26) ={" "}
|
||||
<strong className="text-emerald-700">52</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Cylinder: SA = 2πr² + 2πrh"
|
||||
diagram={<CylSASvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 3, h = 5</p>
|
||||
<p>SA = 2π(9) + 2π(15)</p>
|
||||
<p>
|
||||
SA = 18π + 30π ={" "}
|
||||
<strong className="text-emerald-700">48π ≈ 150.8</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Sphere: SA = 4πr²"
|
||||
diagram={<SphereSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 4</p>
|
||||
<p>
|
||||
SA = 4π(16) ={" "}
|
||||
<strong className="text-emerald-700">64π ≈ 201.1</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Volume */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">Volume</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Volume formulas are provided on the SAT reference sheet.{" "}
|
||||
<strong>Tap any formula</strong> to explore.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaCard
|
||||
formula="Rectangular Prism: V = lwh"
|
||||
diagram={<PrismVolSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>l = 6, w = 3, h = 4</p>
|
||||
<p>
|
||||
V = 6 × 3 × 4 ={" "}
|
||||
<strong className="text-emerald-700">72</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula="Cylinder: V = πr²h"
|
||||
diagram={<CylVolSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 5, h = 8</p>
|
||||
<p>
|
||||
V = π(25)(8) ={" "}
|
||||
<strong className="text-emerald-700">200π ≈ 628.3</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula={
|
||||
<>
|
||||
Cone: V = <Frac n="1" d="3" />
|
||||
πr²h
|
||||
</>
|
||||
}
|
||||
diagram={<ConeSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 4, h = 9</p>
|
||||
<p>V = ⅓ × π(16)(9)</p>
|
||||
<p>
|
||||
V ={" "}
|
||||
<strong className="text-emerald-700">48π ≈ 150.8</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula={
|
||||
<>
|
||||
Sphere: V = <Frac n="4" d="3" />
|
||||
πr³
|
||||
</>
|
||||
}
|
||||
diagram={<SphereVolSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>r = 6</p>
|
||||
<p>V = ⁴⁄₃ × π(216)</p>
|
||||
<p>
|
||||
V ={" "}
|
||||
<strong className="text-emerald-700">288π ≈ 904.8</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormulaCard
|
||||
formula={
|
||||
<>
|
||||
Pyramid: V = <Frac n="1" d="3" />
|
||||
Bh
|
||||
</>
|
||||
}
|
||||
diagram={<PyramidSvg />}
|
||||
example={
|
||||
<>
|
||||
<p>B = 25 (5×5 base), h = 12</p>
|
||||
<p>
|
||||
V = ⅓ × 25 × 12 ={" "}
|
||||
<strong className="text-emerald-700">100</strong>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<CompositeSolidsWidget />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<ScaleFactorWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
A cone is <Frac n="1" d="3" /> of a cylinder with the same base
|
||||
and height. A pyramid is <Frac n="1" d="3" /> of a prism with the
|
||||
same base and height.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Practice */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice
|
||||
</h2>
|
||||
{AREA_VOL_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
{AREA_VOL_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
492
src/pages/student/lessons/CirclePropertiesLesson.tsx
Normal file
492
src/pages/student/lessons/CirclePropertiesLesson.tsx
Normal file
@ -0,0 +1,492 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Target, Layers } from "lucide-react";
|
||||
import CircleTheoremsWidget from "../../../components/lessons/CircleTheoremsWidget";
|
||||
import TangentPropertiesWidget from "../../../components/lessons/TangentPropertiesWidget";
|
||||
import PowerOfPointWidget from "../../../components/lessons/PowerOfPointWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { CIRCLE_PROP_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const CirclePropertiesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-violet-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-violet-600 text-white" : isPast ? "bg-violet-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-violet-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Central vs Inscribed" icon={Target} />
|
||||
<SectionMarker index={1} title="Tangents" icon={Layers} />
|
||||
<SectionMarker index={2} title="Power of a Point" icon={BookOpen} />
|
||||
<SectionMarker index={3} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Central vs Inscribed Angles */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Central vs. Inscribed Angles
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Circle angle theorems are among the highest-frequency SAT topics.
|
||||
The core relationship is simple: angles and arcs are linked by a
|
||||
factor of 2.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
The Central vs. Inscribed Relationship
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-200">
|
||||
<p className="font-bold text-violet-900 mb-1">Central Angle</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Vertex at the <strong>center</strong>. Degree measure equals
|
||||
the intercepted arc.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-violet-50 py-2 rounded text-violet-700 font-bold">
|
||||
∠central = arc°
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Example: Central angle = 80° → arc = 80°
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-indigo-50 rounded-xl p-5 border border-indigo-200">
|
||||
<p className="font-bold text-indigo-900 mb-1">
|
||||
Inscribed Angle
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Vertex on the <strong>circle</strong>. Measure is exactly half
|
||||
the intercepted arc.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-indigo-50 py-2 rounded text-indigo-700 font-bold">
|
||||
∠inscribed = <Frac n="arc°" d="2" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Example: Arc = 120° → inscribed angle = 60°
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Corollaries */}
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Key Corollaries (SAT Favorites)
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800 mb-1">
|
||||
Thales' Theorem
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
An inscribed angle that intercepts a{" "}
|
||||
<strong>semicircle</strong> (its chord is a diameter) is
|
||||
always <strong>90°</strong>. If you see a triangle inscribed
|
||||
in a circle where one side is the diameter, the opposite
|
||||
angle is 90°.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800 mb-1">
|
||||
Inscribed Angles on the Same Arc
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
All inscribed angles intercepting the same arc are equal,
|
||||
regardless of where on the circle the vertex sits.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800 mb-1">
|
||||
Cyclic Quadrilateral
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Opposite angles in a quadrilateral inscribed in a circle sum
|
||||
to 180°. So ∠A + ∠C = 180° and ∠B + ∠D = 180°.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Examples */}
|
||||
<div className="space-y-3">
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 1: Find inscribed angle
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
A central angle is 110°. An inscribed angle intercepts the
|
||||
same arc. Find the inscribed angle.
|
||||
</p>
|
||||
<p>Arc = 110° (central angle equals arc)</p>
|
||||
<p>
|
||||
Inscribed angle = <Frac n="110°" d="2" /> ={" "}
|
||||
<strong className="text-sky-800">55°</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 2: Cyclic quadrilateral
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
Quadrilateral ABCD is inscribed in a circle. ∠A = 75°, ∠B =
|
||||
85°. Find ∠C and ∠D.
|
||||
</p>
|
||||
<p>
|
||||
∠C = 180° − 75° ={" "}
|
||||
<strong className="text-sky-800">105°</strong> (opposite to
|
||||
A)
|
||||
</p>
|
||||
<p>
|
||||
∠D = 180° − 85° ={" "}
|
||||
<strong className="text-sky-800">95°</strong> (opposite to
|
||||
B)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CircleTheoremsWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Tangent Properties{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Tangents */}
|
||||
<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-6">
|
||||
Tangent Properties
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A tangent line touches the circle at exactly one point (the point
|
||||
of tangency). Two critical theorems govern all SAT tangent
|
||||
questions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Two Fundamental Tangent Theorems
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-200">
|
||||
<p className="font-bold text-violet-900 mb-2">
|
||||
Property 1: Tangent-Radius Perpendicularity
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
A radius drawn to the point of tangency is always{" "}
|
||||
<strong>perpendicular</strong> to the tangent line — they form
|
||||
a 90° angle. This creates a right triangle you can use with
|
||||
the Pythagorean theorem.
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-700 mb-1">
|
||||
Worked Example:
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
External point P is 13 units from center O. Radius = 5.
|
||||
Find tangent length PT.
|
||||
</p>
|
||||
<p>PT² + r² = PO² (right angle at T)</p>
|
||||
<p>PT² + 25 = 169</p>
|
||||
<p>
|
||||
PT = √144 ={" "}
|
||||
<strong className="text-violet-700">12</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-200">
|
||||
<p className="font-bold text-violet-900 mb-2">
|
||||
Property 2: Two Tangents from One External Point
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
If two tangent segments are drawn from the same external
|
||||
point, they are <strong>equal in length</strong>. If PA and PB
|
||||
are both tangents from P, then PA = PB.
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-700 mb-1">
|
||||
Worked Example:
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
From external point P, tangent PA = 3x + 2 and tangent PB
|
||||
= 5x − 4.
|
||||
</p>
|
||||
<p>Set equal: 3x + 2 = 5x − 4</p>
|
||||
<p>6 = 2x → x = 3</p>
|
||||
<p>
|
||||
PA = PB = <strong className="text-violet-700">11</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SAT Trap */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-900 mb-1">
|
||||
SAT Trap: Don't Confuse Tangent Line with Tangent Segment
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
The "two tangents are equal" rule applies to the{" "}
|
||||
<em>segments</em> from the external point to the points of
|
||||
tangency — not to the full tangent lines extending beyond the
|
||||
circle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TangentPropertiesWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Power of a Point{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Power of a Point */}
|
||||
<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">
|
||||
Power of a Point
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
"Power of a Point" relates segment lengths when lines pass through
|
||||
or near a circle. Two main cases appear on the SAT.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
The Two Power-of-a-Point Cases
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-200">
|
||||
<p className="font-bold text-violet-900 mb-1">
|
||||
Case 1: Chord-Chord (Inside)
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Two chords intersect inside the circle at point P.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-violet-50 py-2 rounded text-violet-700 font-bold">
|
||||
a × b = c × d
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
a and b are the two segments of one chord; c and d are the two
|
||||
segments of the other.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-indigo-50 rounded-xl p-5 border border-indigo-200">
|
||||
<p className="font-bold text-indigo-900 mb-1">
|
||||
Case 2: Secant-Secant or Tangent-Secant (Outside)
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Two secants, or a tangent and secant, from external point P.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-indigo-50 py-2 rounded text-indigo-700 font-bold">
|
||||
ext₁ × whole₁ = ext₂ × whole₂
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
For tangent: tangent² = ext × whole (since both segments of
|
||||
the tangent chord are equal).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Examples */}
|
||||
<div className="space-y-3">
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 1: Chord-Chord
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
Two chords intersect inside. Chord 1 has segments 4 and 9.
|
||||
Chord 2 has segments 6 and x.
|
||||
</p>
|
||||
<p>4 × 9 = 6 × x</p>
|
||||
<p>
|
||||
36 = 6x → x = <strong className="text-sky-800">6</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 2: Tangent-Secant
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
From external point P: tangent PT = 6, secant passes through
|
||||
circle with external part = 4 and whole length = x.
|
||||
</p>
|
||||
<p>PT² = ext × whole</p>
|
||||
<p>6² = 4 × x</p>
|
||||
<p>
|
||||
36 = 4x → x = <strong className="text-sky-800">9</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 3: Secant-Secant
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
Two secants from P: first has external 3, whole 12. Second
|
||||
has external 4, whole x.
|
||||
</p>
|
||||
<p>3 × 12 = 4 × x</p>
|
||||
<p>
|
||||
36 = 4x → x = <strong className="text-sky-800">9</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PowerOfPointWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{CIRCLE_PROP_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-violet-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-violet-900 font-bold rounded-full hover:bg-violet-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CirclePropertiesLesson;
|
||||
270
src/pages/student/lessons/CirclesLesson.tsx
Normal file
270
src/pages/student/lessons/CirclesLesson.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Circle,
|
||||
Target,
|
||||
Hash,
|
||||
Layers,
|
||||
Ruler,
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import CircleTheoremsWidget from "../../../components/lessons/CircleTheoremsWidget";
|
||||
import TangentPropertiesWidget from "../../../components/lessons/TangentPropertiesWidget";
|
||||
import PowerOfPointWidget from "../../../components/lessons/PowerOfPointWidget";
|
||||
import { CIRCLES_EASY, CIRCLES_MEDIUM } from "../../../data/math/circles";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Equation of a Circle", icon: Circle },
|
||||
{ title: "Completing the Square", icon: Hash },
|
||||
{ title: "Central & Inscribed Angles", icon: Target },
|
||||
{ title: "Arc Length & Sector Area", icon: Layers },
|
||||
{ title: "Tangent Lines", icon: ArrowRight },
|
||||
{ title: "Chord Properties", icon: Ruler },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function CirclesLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Circles"
|
||||
sections={SECTIONS}
|
||||
color="emerald"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Equation */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Equation of a Circle
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The <strong>standard form</strong> of a circle with center (h, k)
|
||||
and radius r:
|
||||
</p>
|
||||
<FormulaBox>(x − h)² + (y − k)² = r²</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Write the Equation" color="emerald">
|
||||
<p>Center (3, −2), radius 5</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">
|
||||
(x − 3)² + (y + 2)² = 25
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Read Center & Radius" color="emerald">
|
||||
<p>(x + 1)² + (y − 4)² = 16</p>
|
||||
<p className="text-slate-500">
|
||||
Center: (−1, 4), radius = √16 ={" "}
|
||||
<strong className="text-emerald-700">4</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
(x − h) means h is positive. So (x + 1)² means h = −1. Watch the
|
||||
signs!
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Completing the Square */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Completing the Square for Circles
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Convert from <strong>general form</strong> (x² + y² + Dx + Ey + F =
|
||||
0) to standard form by completing the square for both x and y.
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm text-slate-700">
|
||||
<p>1. Group x terms together, y terms together</p>
|
||||
<p>2. Complete the square for each group</p>
|
||||
<p>3. Add the same values to both sides</p>
|
||||
<p>4. Write in standard form</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>x² + y² − 6x + 4y − 12 = 0</p>
|
||||
<p className="text-slate-500">
|
||||
(x² − 6x + 9) + (y² + 4y + 4) = 12 + 9 + 4
|
||||
</p>
|
||||
<p className="text-slate-500">(x − 3)² + (y + 2)² = 25</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">
|
||||
Center (3, −2), radius = 5
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Central & Inscribed */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Central & Inscribed Angles
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>central angle</strong> has its vertex at the center — it
|
||||
equals its intercepted arc. An <strong>inscribed angle</strong> has
|
||||
its vertex on the circle — it equals <strong>half</strong> its
|
||||
intercepted arc.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
Inscribed Angle = <Frac n="1" d="2" /> × Intercepted Arc
|
||||
</FormulaBox>
|
||||
<div className="mt-4 bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-900 text-sm mb-1">
|
||||
Key Theorem
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
An inscribed angle that intercepts a semicircle (diameter) is
|
||||
always <strong>90°</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Central angle = 80° → intercepted arc = 80°</p>
|
||||
<p className="text-slate-500">
|
||||
Inscribed angle on same arc ={" "}
|
||||
<strong className="text-emerald-700">40°</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<CircleTheoremsWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Arc & Sector */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Arc Length & Sector Area
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Use the fraction of the circle (central angle ÷ 360°) to find arc
|
||||
length and sector area.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>Arc Length = (θ ÷ 360) × 2πr</FormulaBox>
|
||||
<FormulaBox>Sector Area = (θ ÷ 360) × πr²</FormulaBox>
|
||||
</div>
|
||||
<div className="mt-3 bg-white/60 rounded-lg p-3 border border-emerald-100 text-sm">
|
||||
<p className="font-bold text-emerald-800 mb-1">In Radians</p>
|
||||
<p className="text-slate-700">
|
||||
Arc = rθ | Sector = <Frac n="1" d="2" />
|
||||
r²θ
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>r = 10, θ = 72°</p>
|
||||
<p className="text-slate-500">
|
||||
Arc = (72 ÷ 360) × 20π = <Frac n="1" d="5" /> × 20π = 4π
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Sector = (72 ÷ 360) × 100π ={" "}
|
||||
<strong className="text-emerald-700">20π</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Tangent Lines */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Tangent Lines
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>tangent line</strong> touches the circle at exactly one
|
||||
point and is <strong>perpendicular</strong> to the radius at that
|
||||
point.
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm text-slate-700">
|
||||
<p>• Tangent ⊥ radius at point of tangency</p>
|
||||
<p>
|
||||
• Two tangent segments from the same external point are{" "}
|
||||
<strong>equal in length</strong>
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>
|
||||
External point P, tangent to circle with center O, radius = 5, OP =
|
||||
13
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Tangent² + 5² = 13² (right triangle!)
|
||||
</p>
|
||||
<p className="text-slate-500">Tangent² = 169 − 25 = 144</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">Tangent length = 12</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<TangentPropertiesWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Chords */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Chord Properties
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>chord</strong> has both endpoints on the circle. The
|
||||
diameter is the longest chord.
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm text-slate-700">
|
||||
<p>
|
||||
• A radius perpendicular to a chord <strong>bisects</strong> the
|
||||
chord
|
||||
</p>
|
||||
<p>• Equal chords are equidistant from the center</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Chord of length 24 in a circle of radius 13</p>
|
||||
<p className="text-slate-500">
|
||||
Perpendicular from center bisects chord: half-chord = 12
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
d² + 12² = 13² → d² = 25 →{" "}
|
||||
<strong className="text-emerald-700">d = 5</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<PowerOfPointWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 7: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{CIRCLES_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
{CIRCLES_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
405
src/pages/student/lessons/CollectingDataLesson.tsx
Normal file
405
src/pages/student/lessons/CollectingDataLesson.tsx
Normal file
@ -0,0 +1,405 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
Target,
|
||||
Scale,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
import SamplingVisualizerWidget from "../../../components/lessons/SamplingVisualizerWidget";
|
||||
import StudyDesignWidget from "../../../components/lessons/StudyDesignWidget";
|
||||
import ConfidenceIntervalWidget from "../../../components/lessons/ConfidenceIntervalWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import {
|
||||
COLLECTING_DATA_QUIZ,
|
||||
INFERENCES_QUIZ_DATA,
|
||||
} from "../../../utils/constants";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const CollectingDataLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-amber-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-amber-600 text-white" : isPast ? "bg-amber-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-amber-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const allQuizzes = [...COLLECTING_DATA_QUIZ, ...INFERENCES_QUIZ_DATA];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Sampling & Bias" icon={Scale} />
|
||||
<SectionMarker index={1} title="Study Design" icon={Layers} />
|
||||
<SectionMarker index={2} title="Confidence Intervals" icon={Target} />
|
||||
<SectionMarker index={3} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Sampling & Bias */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Sampling & Bias
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
To generalize results to a population, your sample must be{" "}
|
||||
<strong>representative</strong>. The best way to achieve this is
|
||||
through <strong>random sampling</strong>. Convenience samples
|
||||
(e.g., asking friends) introduce <strong>bias</strong> — they
|
||||
systematically over- or under-represent parts of the population.
|
||||
</p>
|
||||
<div className="mt-5 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Method
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Description
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Bias Risk
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Simple Random
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Every individual has an equal chance of selection
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-700 font-semibold">
|
||||
Very Low
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Stratified
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Population divided into subgroups; random sample from each
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-700 font-semibold">
|
||||
Very Low
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Cluster
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Randomly select entire subgroups (e.g., classrooms)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-700 font-semibold">
|
||||
Low–Medium
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Systematic
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Select every kth individual from a list
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-700 font-semibold">
|
||||
Low–Medium
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Convenience
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Whoever is easiest to reach (friends, passersby)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-rose-700 font-semibold">
|
||||
High
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-900 mb-1">
|
||||
Representative Sample
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Mirrors the population in all key characteristics. Allows you
|
||||
to generalize findings to the whole group. Achieved through
|
||||
random selection.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-4">
|
||||
<p className="font-bold text-rose-900 mb-1">Biased Sample</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Systematically excludes or over-includes certain groups.
|
||||
Results cannot be generalized. Watch for voluntary response
|
||||
and convenience sampling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-amber-50 border-l-4 border-amber-500 rounded-r-lg text-sm">
|
||||
<strong className="text-amber-900">Margin of Error:</strong> Even
|
||||
a well-designed random sample has some uncertainty. The margin of
|
||||
error (e.g., ±3%) tells you the range where the true population
|
||||
value likely falls. Larger samples produce smaller margins of
|
||||
error.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SamplingVisualizerWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Study Design{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Study Design */}
|
||||
<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-6">
|
||||
Study Design: Generalization vs Causation
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Two concepts are critical here. <strong>Random sampling</strong>{" "}
|
||||
determines whether you can extend your conclusions to the broader
|
||||
population. <strong>Random assignment</strong> (used in
|
||||
experiments) determines whether you can claim cause-and-effect.
|
||||
</p>
|
||||
<div className="mt-5 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2"></th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center font-bold">
|
||||
Random Assignment? YES
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center font-bold">
|
||||
Random Assignment? NO
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold bg-amber-50 text-amber-900">
|
||||
Random Sampling? YES
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-emerald-50 text-emerald-800 font-semibold">
|
||||
Generalize + Cause & Effect
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-blue-50 text-blue-800 font-semibold">
|
||||
Generalize only
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold bg-amber-50 text-amber-900">
|
||||
Random Sampling? NO
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-purple-50 text-purple-800 font-semibold">
|
||||
Cause & Effect only (for this group)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-rose-50 text-rose-800 font-semibold">
|
||||
Neither — observe only
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-amber-50 border-l-4 border-amber-500 rounded-r-lg text-sm">
|
||||
<strong className="text-amber-900">SAT Key Rule:</strong>{" "}
|
||||
Observational studies (no random assignment) can show{" "}
|
||||
<em>association</em> but never <em>causation</em>. Only randomized
|
||||
controlled experiments can establish cause-and-effect.
|
||||
</div>
|
||||
</div>
|
||||
<StudyDesignWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Confidence Intervals{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Confidence Intervals & Comparing Groups */}
|
||||
<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">
|
||||
Confidence Intervals & Comparing Groups
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A <strong>Confidence Interval</strong> (Estimate ± Margin of
|
||||
Error) gives a range where the true population value likely lies.
|
||||
When comparing two groups, check for <strong>overlap</strong> — if
|
||||
the intervals do not overlap, you can conclude a significant
|
||||
difference.
|
||||
</p>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-5 space-y-3">
|
||||
<p className="font-bold text-amber-900">
|
||||
Key Rules for Confidence Intervals
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 text-sm">
|
||||
<div className="bg-white rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-1">
|
||||
Intervals Overlap
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Cannot claim a significant difference between the two
|
||||
groups.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-1">
|
||||
Intervals Don't Overlap
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
There is a significant difference — the groups are
|
||||
statistically distinct.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-100 rounded-lg p-3 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
Larger Samples → Smaller Margin of Error
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Increasing the sample size narrows the confidence interval,
|
||||
giving you a more precise estimate.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfidenceIntervalWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{allQuizzes.map((quiz, idx) => (
|
||||
<div key={`quiz-${idx}`} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-amber-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-amber-900 font-bold rounded-full hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectingDataLesson;
|
||||
513
src/pages/student/lessons/CongruenceSimilarityLesson.tsx
Normal file
513
src/pages/student/lessons/CongruenceSimilarityLesson.tsx
Normal file
@ -0,0 +1,513 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Target, Layers } from "lucide-react";
|
||||
import SimilarityWidget from "../../../components/lessons/SimilarityWidget";
|
||||
import SimilarityTestsWidget from "../../../components/lessons/SimilarityTestsWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { SIMILARITY_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const CongruenceSimilarityLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-rose-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-rose-600 text-white" : isPast ? "bg-rose-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-rose-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Congruence Tests" icon={Target} />
|
||||
<SectionMarker index={1} title="Similarity Tests" icon={Layers} />
|
||||
<SectionMarker index={2} title="Proportionality" icon={BookOpen} />
|
||||
<SectionMarker index={3} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Congruence */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Triangle Congruence
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Congruence means <strong>identical</strong> — same size and same
|
||||
shape, every side and every angle matches. You only need one of
|
||||
the following minimal conditions to prove two triangles congruent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
Congruence Tests
|
||||
</h3>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-rose-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-rose-900 text-white">
|
||||
<th className="p-3 text-left">Test</th>
|
||||
<th className="p-3 text-left">Stands For</th>
|
||||
<th className="p-3 text-left">What You Need</th>
|
||||
<th className="p-3 text-left">Key Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-rose-100">
|
||||
<tr className="bg-rose-50">
|
||||
<td className="p-3 font-bold text-rose-800">SSS</td>
|
||||
<td className="p-3">Side-Side-Side</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
All 3 pairs of sides equal
|
||||
</td>
|
||||
<td className="p-3 text-slate-500">Always valid</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold text-rose-800">SAS</td>
|
||||
<td className="p-3">Side-Angle-Side</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
2 sides + angle between them
|
||||
</td>
|
||||
<td className="p-3 text-slate-500">
|
||||
Angle must be included (between the sides)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-rose-50">
|
||||
<td className="p-3 font-bold text-rose-800">ASA</td>
|
||||
<td className="p-3">Angle-Side-Angle</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
2 angles + side between them
|
||||
</td>
|
||||
<td className="p-3 text-slate-500">
|
||||
Side must be between the two angles
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold text-rose-800">AAS</td>
|
||||
<td className="p-3">Angle-Angle-Side</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
2 angles + non-included side
|
||||
</td>
|
||||
<td className="p-3 text-slate-500">
|
||||
Side is NOT between the two angles
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-rose-50">
|
||||
<td className="p-3 font-bold text-rose-800">HL</td>
|
||||
<td className="p-3">Hypotenuse-Leg</td>
|
||||
<td className="p-3 text-slate-600">Hypotenuse + one leg</td>
|
||||
<td className="p-3 text-slate-500">Right triangles ONLY</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-100">
|
||||
<td className="p-3 font-bold text-slate-400 line-through">
|
||||
SSA
|
||||
</td>
|
||||
<td className="p-3 text-slate-400 line-through">
|
||||
Side-Side-Angle
|
||||
</td>
|
||||
<td className="p-3 text-rose-700 font-bold">NOT valid!</td>
|
||||
<td className="p-3 text-rose-600">
|
||||
Ambiguous — two triangles may fit
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* CPCTC */}
|
||||
<div className="bg-white rounded-xl p-5 border border-rose-100">
|
||||
<p className="font-bold text-rose-800 mb-2">
|
||||
CPCTC — Once Proven Congruent
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm">
|
||||
<strong>
|
||||
Corresponding Parts of Congruent Triangles are Congruent.
|
||||
</strong>{" "}
|
||||
Once you've proven △ABC ≅ △DEF by any valid test, you can
|
||||
immediately state that any matching part is equal — side AB =
|
||||
DE, angle B = angle E, etc.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Worked Example */}
|
||||
<div className="bg-sky-50 rounded-xl p-5 border border-sky-200">
|
||||
<p className="font-bold text-sky-800 mb-3">
|
||||
Worked Example: Identifying the Test
|
||||
</p>
|
||||
<div className="text-sm text-slate-700 space-y-2">
|
||||
<p>
|
||||
Two triangles share a common side (reflexive). One pair of
|
||||
other sides is marked equal, and the included angles are both
|
||||
marked as right angles.
|
||||
</p>
|
||||
<p>
|
||||
You have: Right angle (A), common side (S), one equal side
|
||||
(S).
|
||||
</p>
|
||||
<p className="font-bold text-sky-800">
|
||||
→ This is HL (right triangles, hypotenuse and one leg match).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Similarity Tests{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Similarity */}
|
||||
<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-6">
|
||||
Similarity Tests
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Similarity means <strong>same shape, different size</strong>. All
|
||||
corresponding angles are equal, and all corresponding sides are
|
||||
proportional by the same scale factor k.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
The Three Similarity Tests
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 text-center border border-rose-200">
|
||||
<p className="font-bold text-rose-900 mb-1">AA</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
2 pairs of equal angles. Most common on the SAT — if 2 angles
|
||||
match, the third must too (triangle sum).
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 text-center border border-rose-200">
|
||||
<p className="font-bold text-rose-900 mb-1">SAS~</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
2 sides in proportion + the included angle is equal.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 text-center border border-rose-200">
|
||||
<p className="font-bold text-rose-900 mb-1">SSS~</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
All 3 pairs of sides are in the same ratio k.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scale factor consequences */}
|
||||
<div className="bg-white rounded-xl p-5 border border-rose-100">
|
||||
<p className="font-bold text-rose-800 mb-3">
|
||||
What the Scale Factor k Tells You
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-xl border border-rose-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-rose-900 text-white">
|
||||
<th className="p-3 text-left">Measurement</th>
|
||||
<th className="p-3 text-left">Rule</th>
|
||||
<th className="p-3 text-left">Example (k = 3)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-rose-100">
|
||||
<tr className="bg-rose-50">
|
||||
<td className="p-3">Sides / Lengths</td>
|
||||
<td className="p-3 font-mono">× k</td>
|
||||
<td className="p-3">Side 4 → 12</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3">Perimeters</td>
|
||||
<td className="p-3 font-mono">× k</td>
|
||||
<td className="p-3">Perimeter 18 → 54</td>
|
||||
</tr>
|
||||
<tr className="bg-rose-50">
|
||||
<td className="p-3">Areas</td>
|
||||
<td className="p-3 font-mono">× k²</td>
|
||||
<td className="p-3">Area 10 → 90</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3">Angles</td>
|
||||
<td className="p-3 font-mono">unchanged</td>
|
||||
<td className="p-3">All angles the same</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Finding a missing side */}
|
||||
<div className="bg-sky-50 rounded-xl p-5 border border-sky-200">
|
||||
<p className="font-bold text-sky-800 mb-3">
|
||||
Worked Example: Find the Missing Side
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>△ABC ~ △DEF. AB = 6, BC = 9, DE = 4. Find EF.</p>
|
||||
<p>
|
||||
k = <Frac n="DE" d="AB" /> = <Frac n="4" d="6" /> ={" "}
|
||||
<Frac n="2" d="3" />
|
||||
</p>
|
||||
<p>
|
||||
EF = BC × k = 9 × <Frac n="2" d="3" /> ={" "}
|
||||
<strong className="text-sky-800">6</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SimilarityTestsWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Proportionality{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Proportionality */}
|
||||
<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">
|
||||
Triangle Proportionality
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The <strong>Triangle Proportionality Theorem</strong> states: a
|
||||
line parallel to one side of a triangle cuts the other two sides
|
||||
proportionally, creating two nested similar triangles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
The Proportionality Setup
|
||||
</h3>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-rose-100">
|
||||
<p className="font-bold text-rose-800 mb-3">If DE ∥ BC, then:</p>
|
||||
<div className="grid md:grid-cols-3 gap-3 text-sm">
|
||||
<div className="bg-rose-50 rounded-lg p-3 border border-rose-100 text-center">
|
||||
<p className="font-mono font-bold text-rose-700">
|
||||
<Frac n="AD" d="DB" /> = <Frac n="AE" d="EC" />
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
sides cut proportionally
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 rounded-lg p-3 border border-rose-100 text-center">
|
||||
<p className="font-mono font-bold text-rose-700">
|
||||
△ADE ~ △ABC
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
by AA similarity
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 rounded-lg p-3 border border-rose-100 text-center">
|
||||
<p className="font-mono font-bold text-rose-700">
|
||||
<Frac n="AD" d="AB" /> = <Frac n="AE" d="AC" /> ={" "}
|
||||
<Frac n="DE" d="BC" />
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
all ratios equal
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Examples */}
|
||||
<div className="space-y-3">
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 1: Find a segment length
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>DE ∥ BC. AD = 4, DB = 6, AE = 5. Find EC.</p>
|
||||
<p>
|
||||
<Frac n="AD" d="DB" /> = <Frac n="AE" d="EC" />
|
||||
</p>
|
||||
<p>
|
||||
<Frac n="4" d="6" /> = <Frac n="5" d="EC" />
|
||||
</p>
|
||||
<p>
|
||||
EC = <Frac n="5 × 6" d="4" /> = <Frac n="30" d="4" /> ={" "}
|
||||
<strong className="text-sky-800">7.5</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 2: Find side DE
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>DE ∥ BC. AD = 3, AB = 9, BC = 12. Find DE.</p>
|
||||
<p>
|
||||
k = <Frac n="AD" d="AB" /> = <Frac n="3" d="9" /> ={" "}
|
||||
<Frac n="1" d="3" />
|
||||
</p>
|
||||
<p>
|
||||
DE = BC × k = 12 × <Frac n="1" d="3" /> ={" "}
|
||||
<strong className="text-sky-800">4</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 3: Find area ratio
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
△ADE ~ △ABC with k = <Frac n="1" d="3" />. If Area(ABC) =
|
||||
27, find Area(ADE).
|
||||
</p>
|
||||
<p>
|
||||
Area scales by k² = (<Frac n="1" d="3" />
|
||||
)² = <Frac n="1" d="9" />
|
||||
</p>
|
||||
<p>
|
||||
Area(ADE) = 27 × <Frac n="1" d="9" /> ={" "}
|
||||
<strong className="text-sky-800">3</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
SAT Strategy: Spot the Parallel Line
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
When you see a triangle with a line drawn parallel to one side
|
||||
(look for tick marks or stated conditions), immediately set up a
|
||||
proportion using the segments. This is usually solvable in 2–3
|
||||
steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SimilarityWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{SIMILARITY_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-rose-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-rose-900 font-bold rounded-full hover:bg-rose-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CongruenceSimilarityLesson;
|
||||
289
src/pages/student/lessons/DataAnalysisLesson.tsx
Normal file
289
src/pages/student/lessons/DataAnalysisLesson.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
Target,
|
||||
BarChart3,
|
||||
Calculator,
|
||||
} from "lucide-react";
|
||||
import DataModifierWidget from "../../../components/lessons/DataModifierWidget";
|
||||
import BoxPlotComparisonWidget from "../../../components/lessons/BoxPlotComparisonWidget";
|
||||
import ConfidenceIntervalWidget from "../../../components/lessons/ConfidenceIntervalWidget";
|
||||
import StudyDesignWidget from "../../../components/lessons/StudyDesignWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import {
|
||||
CENTER_SPREAD_QUIZ_DATA,
|
||||
DISTRIBUTIONS_QUIZ_DATA,
|
||||
INFERENCES_QUIZ_DATA,
|
||||
} from "../../../utils/constants";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const DataAnalysisLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) {
|
||||
setActiveSection(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -60% 0px",
|
||||
},
|
||||
);
|
||||
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${
|
||||
isActive
|
||||
? "bg-white shadow-md border border-amber-100"
|
||||
: "hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
|
||||
isActive
|
||||
? "bg-amber-600 text-white"
|
||||
: isPast
|
||||
? "bg-amber-400 text-white"
|
||||
: "bg-slate-200 text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-amber-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DATA_ANALYSIS_QUIZ_DATA = [
|
||||
...CENTER_SPREAD_QUIZ_DATA,
|
||||
...DISTRIBUTIONS_QUIZ_DATA,
|
||||
...INFERENCES_QUIZ_DATA,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Data Changes" icon={Calculator} />
|
||||
<SectionMarker index={1} title="Distributions" icon={BarChart3} />
|
||||
<SectionMarker index={2} title="Inferences" icon={Target} />
|
||||
<SectionMarker index={3} title="Study Design" icon={BookOpen} />
|
||||
<SectionMarker index={4} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Data Changes */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Effects of Data Changes
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The SAT often asks how the Mean, Median, and Standard Deviation
|
||||
change without doing the full math.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-2">
|
||||
<li>
|
||||
<strong>Add Constant:</strong> Center shifts, Spread stays same.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Multiply Constant:</strong> Center and Spread both
|
||||
scale.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Outliers:</strong> Pull the Mean strongly, but the
|
||||
Median is resistant.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<DataModifierWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Comparing Distributions{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Distributions */}
|
||||
<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-6">
|
||||
Comparing Distributions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
<strong>Box Plots</strong> are the fastest way to compare Median
|
||||
and Spread (IQR & Range). Remember: The box contains the middle
|
||||
50% of the data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BoxPlotComparisonWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Data Inferences{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Inferences */}
|
||||
<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">
|
||||
Data Inferences & Margin of Error
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A <strong>Confidence Interval</strong> (Estimate ± Margin of
|
||||
Error) gives a range where the true population value likely lies.
|
||||
<br />
|
||||
To compare two groups, check for <strong>overlap</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ConfidenceIntervalWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Study Design{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Study Design */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Study Design: Conclusion Logic
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Two magic phrases determine what you can conclude:
|
||||
<br />
|
||||
1. <strong>Random Sampling</strong> allows Generalization.
|
||||
<br />
|
||||
2. <strong>Random Assignment</strong> allows Causation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<StudyDesignWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(4)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 5: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[4] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{DATA_ANALYSIS_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-amber-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-amber-900 font-bold rounded-full hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisLesson;
|
||||
768
src/pages/student/lessons/DataRepresentationLesson.tsx
Normal file
768
src/pages/student/lessons/DataRepresentationLesson.tsx
Normal file
@ -0,0 +1,768 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
Target,
|
||||
BarChart3,
|
||||
Calculator,
|
||||
Layers,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import FrequencyMeanWidget from "../../../components/lessons/FrequencyMeanWidget";
|
||||
import HistogramBuilderWidget from "../../../components/lessons/HistogramBuilderWidget";
|
||||
import BoxPlotAnatomyWidget from "../../../components/lessons/BoxPlotAnatomyWidget";
|
||||
import BoxPlotComparisonWidget from "../../../components/lessons/BoxPlotComparisonWidget";
|
||||
import DataModifierWidget from "../../../components/lessons/DataModifierWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import {
|
||||
DATA_REP_QUIZ_DATA,
|
||||
CENTER_SPREAD_QUIZ_DATA,
|
||||
DISTRIBUTIONS_QUIZ_DATA,
|
||||
} from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const DataRepresentationLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) {
|
||||
setActiveSection(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${
|
||||
isActive
|
||||
? "bg-white shadow-md border border-amber-100"
|
||||
: "hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
|
||||
isActive
|
||||
? "bg-amber-600 text-white"
|
||||
: isPast
|
||||
? "bg-amber-400 text-white"
|
||||
: "bg-slate-200 text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-amber-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const allQuizzes = [
|
||||
...DATA_REP_QUIZ_DATA,
|
||||
...CENTER_SPREAD_QUIZ_DATA,
|
||||
...DISTRIBUTIONS_QUIZ_DATA,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Frequency & Mean" icon={Calculator} />
|
||||
<SectionMarker index={1} title="Histograms" icon={BarChart3} />
|
||||
<SectionMarker index={2} title="Box Plots" icon={Layers} />
|
||||
<SectionMarker index={3} title="Center & Spread" icon={TrendingUp} />
|
||||
<SectionMarker
|
||||
index={4}
|
||||
title="Effects of Change"
|
||||
icon={Calculator}
|
||||
/>
|
||||
<SectionMarker index={5} title="Comparisons" icon={Target} />
|
||||
<SectionMarker index={6} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Frequency & Mean */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Frequency Tables & Weighted Mean
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
When calculating the mean from a table, you must use a{" "}
|
||||
<strong>weighted mean</strong>. Simply adding the values in the
|
||||
first column is a common trap! You must multiply each value by its
|
||||
frequency first.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-amber-900 mb-3">
|
||||
The Weighted Mean Formula
|
||||
</h3>
|
||||
<div className="bg-white rounded-xl p-4 text-center mb-4 border border-amber-100">
|
||||
<p className="text-xl font-mono font-bold text-amber-800">
|
||||
Weighted Mean ={" "}
|
||||
<Frac n="Σ(value × frequency)" d="Σ(frequency)" />
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-slate-600 text-base">
|
||||
For each row, multiply the value by its frequency count. Sum all
|
||||
those products, then divide by the total number of data points
|
||||
(the sum of all frequencies).
|
||||
</p>
|
||||
<div className="mt-4 bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="text-red-800 font-bold text-sm">
|
||||
Common SAT Trap: Do NOT average the values in the first column
|
||||
directly. That ignores how many times each value appears and
|
||||
will almost always give the wrong answer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FrequencyMeanWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Histograms{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Histograms */}
|
||||
<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-6">
|
||||
Histograms
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Histograms group data into <strong>bins</strong> (intervals). Each
|
||||
bar covers a range of values, and the height of the bar tells you
|
||||
how many data points fall in that range. All bins have equal width
|
||||
on the SAT.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
Key Histogram Concepts
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-1">
|
||||
Frequency (Count)
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm">
|
||||
The raw number of data points in a bin. Read directly from the
|
||||
y-axis when it is labeled "Frequency" or "Count".
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-1">
|
||||
Relative Frequency (Percent)
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm">
|
||||
Each bin's count divided by the total number of data points.
|
||||
Formula:{" "}
|
||||
<strong>
|
||||
Relative Frequency = <Frac n="count" d="total" />
|
||||
</strong>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-100 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
SAT Trick: Count vs. Percent Switch
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm">
|
||||
The SAT frequently presents a histogram in one form (frequency)
|
||||
and asks a question that requires the other (relative
|
||||
frequency). Always check the y-axis label carefully.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HistogramBuilderWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Box Plots{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Box Plots */}
|
||||
<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">
|
||||
Anatomy of a Box Plot
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A Box Plot visualizes the <strong>5-Number Summary</strong>: Min,
|
||||
Q1, Median, Q3, Max. The box itself represents the{" "}
|
||||
<strong>IQR</strong> (Interquartile Range), which contains the
|
||||
middle 50% of the data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
The 5-Number Summary
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-5 gap-3">
|
||||
{[
|
||||
{ label: "Min", desc: "Smallest value (left whisker)" },
|
||||
{ label: "Q1", desc: "25th percentile (left edge of box)" },
|
||||
{ label: "Median", desc: "50th percentile (line inside box)" },
|
||||
{ label: "Q3", desc: "75th percentile (right edge of box)" },
|
||||
{ label: "Max", desc: "Largest value (right whisker)" },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="bg-white rounded-xl p-3 border border-amber-100 text-center"
|
||||
>
|
||||
<p className="font-bold text-amber-800 text-lg">
|
||||
{item.label}
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-2">
|
||||
Interquartile Range (IQR)
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-2">
|
||||
The IQR measures the spread of the middle 50% of the data.
|
||||
</p>
|
||||
<p className="font-mono text-amber-700 font-bold">
|
||||
IQR = Q3 − Q1
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-100 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
Outlier Detection Rule
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm mb-2">
|
||||
A data point is an outlier if it falls outside these boundaries:
|
||||
</p>
|
||||
<div className="space-y-1 font-mono text-sm">
|
||||
<p className="text-red-700 font-bold">
|
||||
Lower Bound: Q1 − 1.5 × IQR
|
||||
</p>
|
||||
<p className="text-red-700 font-bold">
|
||||
Upper Bound: Q3 + 1.5 × IQR
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BoxPlotAnatomyWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Center & Spread{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Center & Spread (from CenterSpreadLesson) */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Measures of Center & Spread
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The SAT always tests your ability to interpret center (where data
|
||||
clusters) and spread (how far data varies). Understanding what
|
||||
each measure is resistant or sensitive to is critical.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
Measures of Center
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-200">
|
||||
<p className="font-bold text-amber-900 mb-1">Mean (Average)</p>
|
||||
<div className="font-mono text-center bg-amber-50 py-2 rounded text-amber-700 font-bold text-sm mb-2">
|
||||
<Frac n="Sum" d="Count" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Add all values, divide by how many there are. Sensitive to
|
||||
outliers — one extreme value pulls the mean.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-200">
|
||||
<p className="font-bold text-amber-900 mb-1">Median</p>
|
||||
<div className="font-mono text-center bg-amber-50 py-2 rounded text-amber-700 font-bold text-sm mb-2">
|
||||
Middle value (sorted)
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Sort the data. Middle value if odd count; average of middle
|
||||
two if even count. Resistant to outliers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-200">
|
||||
<p className="font-bold text-amber-900 mb-1">Mode</p>
|
||||
<div className="font-mono text-center bg-amber-50 py-2 rounded text-amber-700 font-bold text-sm mb-2">
|
||||
Most frequent value
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
The value that appears most often. A dataset can have no mode,
|
||||
one mode, or multiple modes. Rare on SAT.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-3">
|
||||
Worked Example: Mean vs. Median with an Outlier
|
||||
</p>
|
||||
<div className="bg-amber-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-amber-800 mb-2">
|
||||
Dataset: {"{"}5, 7, 8, 9, 10, 11, 95{"}"}
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>Sum = 5 + 7 + 8 + 9 + 10 + 11 + 95 = 145</p>
|
||||
<p>
|
||||
Mean = <Frac n="145" d="7" /> ≈{" "}
|
||||
<strong className="text-rose-700">20.7</strong> ← pulled
|
||||
toward 95
|
||||
</p>
|
||||
<p>
|
||||
Median = <strong className="text-emerald-700">9</strong> ←
|
||||
middle value, not affected by 95
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-amber-900 pt-2">
|
||||
Measures of Spread
|
||||
</h3>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-amber-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-900 text-white">
|
||||
<th className="p-3 text-left">Measure</th>
|
||||
<th className="p-3 text-left">Formula</th>
|
||||
<th className="p-3 text-left">Sensitive to Outliers?</th>
|
||||
<th className="p-3 text-left">Best Used With</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-amber-100">
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">Range</td>
|
||||
<td className="p-3 font-mono text-amber-700">Max − Min</td>
|
||||
<td className="p-3 text-rose-700 font-bold">
|
||||
Yes — very sensitive
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">Simple comparisons</td>
|
||||
</tr>
|
||||
<tr className="bg-amber-50">
|
||||
<td className="p-3 font-bold">IQR</td>
|
||||
<td className="p-3 font-mono text-amber-700">Q3 − Q1</td>
|
||||
<td className="p-3 text-emerald-700 font-bold">
|
||||
No — resistant
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">Median; skewed data</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">Standard Deviation</td>
|
||||
<td className="p-3 font-mono text-amber-700">
|
||||
Avg distance from mean
|
||||
</td>
|
||||
<td className="p-3 text-rose-700 font-bold">
|
||||
Yes — sensitive
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">Mean; symmetric data</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-3">
|
||||
Skew Direction → Mean vs. Median
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 text-sm">
|
||||
<div className="bg-rose-50 rounded-lg p-3 border border-rose-100">
|
||||
<p className="font-bold text-rose-800 mb-1">
|
||||
Right-Skewed (Positive Skew)
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
A long tail extends to the right. Mean is pulled right
|
||||
(larger) by high outliers.
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1 text-rose-700">
|
||||
Mean > Median > Mode
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-1">
|
||||
Left-Skewed (Negative Skew)
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
A long tail extends to the left. Mean is pulled left
|
||||
(smaller) by low outliers.
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1 text-blue-700">
|
||||
Mean < Median < Mode
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(4)}
|
||||
className="mt-4 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Effects of Change{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 5: Effects of Data Changes (from CenterSpreadLesson) */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[4] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Effects of Data Changes
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The SAT loves questions that ask what happens to mean, median, and
|
||||
standard deviation after modifying data — without making you
|
||||
recalculate everything. Memorize these rules.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
The Rules Table
|
||||
</h3>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-amber-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-900 text-white">
|
||||
<th className="p-3 text-left">Operation</th>
|
||||
<th className="p-3 text-left">Mean</th>
|
||||
<th className="p-3 text-left">Median</th>
|
||||
<th className="p-3 text-left">Std Dev / Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-amber-100">
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">
|
||||
Add constant k to every value
|
||||
</td>
|
||||
<td className="p-3 font-mono text-amber-700">Mean + k</td>
|
||||
<td className="p-3 font-mono text-amber-700">Median + k</td>
|
||||
<td className="p-3 font-bold text-emerald-700">
|
||||
No change
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-amber-50">
|
||||
<td className="p-3 font-bold">Multiply every value by k</td>
|
||||
<td className="p-3 font-mono text-amber-700">Mean × k</td>
|
||||
<td className="p-3 font-mono text-amber-700">Median × k</td>
|
||||
<td className="p-3 font-bold text-amber-700">
|
||||
× k (scales too)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">Add a high outlier</td>
|
||||
<td className="p-3 text-rose-700">Increases</td>
|
||||
<td className="p-3 text-emerald-700">Barely changes</td>
|
||||
<td className="p-3 text-rose-700">Increases</td>
|
||||
</tr>
|
||||
<tr className="bg-amber-50">
|
||||
<td className="p-3 font-bold">Remove a high outlier</td>
|
||||
<td className="p-3 text-rose-700">
|
||||
Decreases (toward center)
|
||||
</td>
|
||||
<td className="p-3 text-emerald-700">Barely changes</td>
|
||||
<td className="p-3 text-rose-700">Decreases</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-3">
|
||||
Why Adding k Doesn't Change Spread
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Standard deviation measures how far values are from the mean. If
|
||||
you add k to every value, the mean also shifts by k — so every
|
||||
distance from the mean stays the same.
|
||||
</p>
|
||||
<div className="bg-amber-50 rounded-lg p-3 text-xs font-mono text-slate-700">
|
||||
<p>
|
||||
Dataset: {"{"}2, 4, 6{"}"} → Mean = 4, SD ≈ 1.63
|
||||
</p>
|
||||
<p>
|
||||
Add 10: {"{"}12, 14, 16{"}"} → Mean = 14, SD ≈ 1.63
|
||||
(unchanged)
|
||||
</p>
|
||||
<p>
|
||||
Multiply by 2: {"{"}4, 8, 12{"}"} → Mean = 8, SD ≈ 3.27
|
||||
(doubled)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 1: What happens when an outlier is removed?
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
Dataset: {"{"}2, 3, 4, 5, 6, 50{"}"}. Mean ≈ 11.7, Median =
|
||||
4.5
|
||||
</p>
|
||||
<p>
|
||||
Remove 50: {"{"}2, 3, 4, 5, 6{"}"}. Mean = 4, Median = 4
|
||||
</p>
|
||||
<p className="text-sky-800 font-bold">
|
||||
Mean decreased significantly. Median barely changed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example 2: Scores shifted
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>Teacher adds 5 bonus points to every student's score.</p>
|
||||
<p>
|
||||
Class mean was 72 → new mean ={" "}
|
||||
<strong className="text-sky-800">77</strong>
|
||||
</p>
|
||||
<p>
|
||||
Class median was 74 → new median ={" "}
|
||||
<strong className="text-sky-800">79</strong>
|
||||
</p>
|
||||
<p>
|
||||
Standard deviation:{" "}
|
||||
<strong className="text-sky-800">no change</strong> (spread
|
||||
unchanged)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataModifierWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(5)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Comparisons{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 6: Comparing Distributions */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[5] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Comparing Distributions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
When comparing two datasets, always address two dimensions:{" "}
|
||||
<strong>Center</strong> and <strong>Spread</strong>. The SAT often
|
||||
asks you to compare groups using both.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
What to Compare
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-2">Center</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
<strong>Median</strong> — use when data is skewed or has
|
||||
outliers; it is resistant to extreme values.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Mean</strong> — use when data is roughly symmetric;
|
||||
it accounts for all values but is pulled by outliers.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 mb-2">Spread</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
<strong>IQR</strong> — measures spread of the middle 50%;
|
||||
resistant to outliers. Preferred with median.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Standard Deviation (SD)</strong> — measures average
|
||||
distance from the mean; sensitive to outliers. Preferred
|
||||
with mean.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-100 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
SAT Language to Watch For
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm">
|
||||
"Which group has greater variability?" → Compare IQR or SD
|
||||
(larger value = more spread out).
|
||||
<br />
|
||||
"Which group has a higher typical value?" → Compare medians
|
||||
or means.
|
||||
<br />
|
||||
"Are the distributions similar in shape?" → Look at whether
|
||||
both are symmetric, skewed left, or skewed right.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BoxPlotComparisonWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(6)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 7: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[6] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{allQuizzes.map((quiz, idx) => (
|
||||
<div key={`quiz-${idx}`} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-amber-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-amber-900 font-bold rounded-full hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataRepresentationLesson;
|
||||
429
src/pages/student/lessons/EBRWCommasLesson.tsx
Normal file
429
src/pages/student/lessons/EBRWCommasLesson.tsx
Normal file
@ -0,0 +1,429 @@
|
||||
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.ElementType;
|
||||
}) => {
|
||||
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: "Subject–Verb 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;
|
||||
309
src/pages/student/lessons/EBRWCraftStructureLesson.tsx
Normal file
309
src/pages/student/lessons/EBRWCraftStructureLesson.tsx
Normal file
@ -0,0 +1,309 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
TEXT_STRUCTURE_EASY,
|
||||
TEXT_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/text-structure-purpose";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question: "What is the primary purpose of sentence 4 in this passage?",
|
||||
passage: [
|
||||
"Automation has already transformed manufacturing, eliminating millions of repetitive assembly-line jobs over the past three decades.",
|
||||
"Optimists argue that, just as industrialization created new types of work, AI will generate new categories of employment.",
|
||||
"Economists at the Brookings Institution project that 36 million American jobs face high exposure to automation within the next decade.",
|
||||
"Yet history offers a cautionary note: the transition from agricultural to industrial economies, though ultimately beneficial, caused decades of widespread unemployment and social upheaval.",
|
||||
"Without deliberate policy intervention, the current transition may prove equally disruptive.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
'Sentence 4 serves to qualify and complicate the optimists\' argument in sentence 2 — it concedes that historical transitions were ultimately beneficial, but shows they were also extremely disruptive. This is a "concession and complication" move. On the SAT, look for words like "yet," "however," "though" that signal this function.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Which sentence most clearly establishes the author's central perspective in this passage?",
|
||||
passage: [
|
||||
"In 1905, Einstein published four papers that would revolutionize physics.",
|
||||
"Among them was the special theory of relativity, which overturned Newtonian mechanics that had stood unchallenged for over 200 years.",
|
||||
"Physicists at the time were deeply skeptical — the implications were simply too radical to accept without extensive verification.",
|
||||
"Science, at its best, is not a collection of fixed truths but an evolving conversation shaped by evidence, argument, and the occasional genius willing to question everything.",
|
||||
"Einstein's annus mirabilis remains the most dramatic example of this process in modern science.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
"Sentence 4 is where the author steps back from narrating events to state their own perspective on what science is: an evolving conversation. This is a craft move — transitioning from historical account to authorial commentary. The SAT often places the author's stated viewpoint in the middle or end of a passage, not the beginning.",
|
||||
},
|
||||
{
|
||||
question: "What is the structural function of sentence 2 in this passage?",
|
||||
passage: [
|
||||
"Digital misinformation spreads with unprecedented speed on social media platforms.",
|
||||
"A landmark MIT study found that false news spreads six times faster on Twitter than true news.",
|
||||
"The researchers attribute this difference to the novelty and emotional resonance of misinformation — falsehoods are simply more surprising and engaging than accurate reporting.",
|
||||
"This poses serious challenges for democracies that depend on an informed citizenry.",
|
||||
"Platform companies and legislators are still struggling to find effective interventions.",
|
||||
],
|
||||
evidenceIndex: 1,
|
||||
explanation:
|
||||
"Sentence 2 provides specific quantitative evidence (the MIT study) to support the claim made in sentence 1. Its function is to substantiate — to give a concrete, credible example that proves the opening assertion. On SAT craft questions, sentences that cite studies, statistics, or expert sources typically function as evidence or support.",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWCraftStructureLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-fuchsia-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-fuchsia-600 text-white" : isPast ? "bg-fuchsia-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-fuchsia-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="Craft & Purpose" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Craft & Purpose */}
|
||||
<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-fuchsia-100 text-fuchsia-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Craft & Structure
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Craft & Purpose
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
SAT craft questions ask WHY the author made a choice — not WHAT the
|
||||
text says. Master the four analytical lenses below to handle every
|
||||
question type.
|
||||
</p>
|
||||
|
||||
{/* Rule Grid */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-fuchsia-50 border border-fuchsia-200 space-y-5">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Four Analytical Lenses
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{[
|
||||
{
|
||||
title: "Author's Purpose",
|
||||
body: "Why did the author include THIS paragraph, sentence, or detail? Options: to introduce, contrast, provide evidence, concede, qualify, or conclude.",
|
||||
},
|
||||
{
|
||||
title: "Transition Function",
|
||||
body: "What does this sentence DO in relation to what came before? Contrast? Continuation? Consequence?",
|
||||
},
|
||||
{
|
||||
title: "Point of View",
|
||||
body: "What is the narrator's or author's perspective? First person (I/we) vs. third person (objective or omniscient).",
|
||||
},
|
||||
{
|
||||
title: "Rhetorical Effect",
|
||||
body: "How does the author create impact? Through analogy, anecdote, expert citation, rhetorical question, or repetition?",
|
||||
},
|
||||
].map((rule, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white rounded-xl p-4 border border-fuchsia-100"
|
||||
>
|
||||
<p className="text-sm font-bold text-fuchsia-800 mb-1">
|
||||
{rule.title}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
{rule.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Static annotation visual */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-fuchsia-50 border border-fuchsia-200 space-y-3">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900 mb-1">
|
||||
How to Annotate a Craft Question
|
||||
</h3>
|
||||
<div className="bg-fuchsia-100 rounded-xl p-4 border border-fuchsia-200">
|
||||
<p className="text-xs font-bold text-fuchsia-800 mb-1">
|
||||
Question type:
|
||||
</p>
|
||||
<p className="text-sm text-fuchsia-900">
|
||||
"The main purpose of the third paragraph is to..."
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-100 rounded-xl p-4 border border-green-200">
|
||||
<p className="text-xs font-bold text-green-800 mb-1">Strategy:</p>
|
||||
<p className="text-sm text-green-900">
|
||||
Read the paragraph. Then ask: does it introduce an idea, provide
|
||||
evidence, counter an argument, or conclude?
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 rounded-xl p-4 border border-orange-200">
|
||||
<p className="text-xs font-bold text-orange-800 mb-1">Trap:</p>
|
||||
<p className="text-sm text-orange-900">
|
||||
Confusing a detail's FUNCTION with its CONTENT. "To describe X"
|
||||
is content. "To provide evidence for the claim in paragraph 2"
|
||||
is function.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trap callout */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-5 mb-8">
|
||||
<p className="text-sm font-bold text-red-800 mb-1">
|
||||
SAT Craft Question Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
SAT craft questions ask WHY the author made a choice, not WHAT the
|
||||
text says. Always frame your thinking in terms of author intent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden Rule */}
|
||||
<div className="bg-fuchsia-900 rounded-2xl p-6 mb-8">
|
||||
<p className="text-xs font-bold text-fuchsia-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-base font-bold text-white">
|
||||
Every element has a structural job. Identify it: introduce,
|
||||
evidence, concede, contrast, conclude, qualify, or illustrate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-fuchsia-600 font-bold hover:text-fuchsia-800 transition-colors"
|
||||
>
|
||||
Next: Evidence Hunter{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Evidence Hunter */}
|
||||
<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">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Read each passage. Identify the sentence that serves the stated
|
||||
structural purpose, then reveal the explanation.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-fuchsia-600 font-bold hover:text-fuchsia-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{TEXT_STRUCTURE_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
{TEXT_STRUCTURE_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-fuchsia-900 text-white font-bold rounded-full hover:bg-fuchsia-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWCraftStructureLesson;
|
||||
448
src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx
Normal file
448
src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx
Normal file
@ -0,0 +1,448 @@
|
||||
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: "Em Dash — Nonessential Phrase",
|
||||
segments: [
|
||||
{ text: "The results", type: "subject", label: "Subject" },
|
||||
{ text: " —", type: "punct" },
|
||||
{
|
||||
text: " gathered over three years",
|
||||
type: "modifier",
|
||||
label: "Nonessential Info (must be paired)",
|
||||
},
|
||||
{ text: " —", type: "punct" },
|
||||
{ text: " confirmed the hypothesis", type: "verb", label: "Predicate" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Possession: team's",
|
||||
segments: [
|
||||
{ text: "The", type: "ic", label: "" },
|
||||
{ text: " team's", type: "subject", label: "Add 's for possession" },
|
||||
{ text: " report", type: "ic", label: "" },
|
||||
{ text: " was submitted on time", type: "verb", label: "Predicate" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Contraction: they're / it's",
|
||||
segments: [
|
||||
{
|
||||
text: "They're",
|
||||
type: "conjunction",
|
||||
label: "Contraction: they + are",
|
||||
},
|
||||
{ text: " reviewing", type: "verb", label: "" },
|
||||
{ text: " the data", type: "ic", label: "" },
|
||||
{ text: " because", type: "dc", label: "" },
|
||||
{ text: " it's", type: "conjunction", label: "Contraction: it + is" },
|
||||
{ text: " inconclusive", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const DASH_SUBTREE: TreeNode = {
|
||||
id: "dash",
|
||||
question: "Is there already an em dash somewhere else in the sentence?",
|
||||
hint: "Em dashes must come in PAIRS when setting off a nonessential phrase in the middle of a sentence.",
|
||||
yesLabel: "Yes — there's a dash elsewhere in the sentence",
|
||||
noLabel: "No — this is the only dash",
|
||||
yes: {
|
||||
id: "dash-pair",
|
||||
result:
|
||||
"✓ Match the punctuation. If an em dash opens a nonessential phrase, another em dash must close it.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[IC] — [nonessential phrase] — [rest of IC]",
|
||||
},
|
||||
no: {
|
||||
id: "dash-single",
|
||||
question:
|
||||
"Is the dash at the END of the sentence introducing an explanation or list?",
|
||||
yesLabel: "Yes — introducing what follows",
|
||||
noLabel: "No — it's in the middle of a sentence",
|
||||
yes: {
|
||||
id: "dash-end",
|
||||
result:
|
||||
"✓ A single em dash at the end correctly introduces an explanation, list, or dramatic pause.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Complete sentence] — [explanation or list]",
|
||||
},
|
||||
no: {
|
||||
id: "dash-mid",
|
||||
result:
|
||||
"⚠ A single em dash in the middle of a sentence is incorrect — it must be paired with another em dash, or converted to commas.",
|
||||
resultType: "warning",
|
||||
ruleRef: "Fix: use matching dashes or commas",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const APOSTROPHE_SUBTREE: TreeNode = {
|
||||
id: "apostrophe",
|
||||
question: "Can you substitute 'it is' or 'they are' in place of the word?",
|
||||
hint: "Try substituting: 'its / it is' — if 'it is' fits, use 'it\u2019s'. If not, use 'its' (possessive).",
|
||||
yesLabel: "Yes — contraction fits (it is / they are)",
|
||||
noLabel: "No — it's showing ownership",
|
||||
yes: {
|
||||
id: "contraction",
|
||||
result:
|
||||
"✓ Use the contraction form: it's (it is), they're (they are), who's (who is).",
|
||||
resultType: "correct",
|
||||
ruleRef: "Contraction: it's = it + is | they're = they + are",
|
||||
},
|
||||
no: {
|
||||
id: "possession",
|
||||
question: "Does the noun END in -s already (plural)?",
|
||||
yesLabel: "Yes — plural noun ending in -s",
|
||||
noLabel: "No — singular noun",
|
||||
yes: {
|
||||
id: "plural-poss",
|
||||
result:
|
||||
"✓ Add only an apostrophe after the -s: students' (not students's).",
|
||||
resultType: "correct",
|
||||
ruleRef: "Plural possession: [noun]s' [thing]",
|
||||
},
|
||||
no: {
|
||||
id: "singular-poss",
|
||||
result: "✓ Add 's to the singular noun: scientist's, team's.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Singular possession: [noun]'s [thing]",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const DASH_APOSTROPHE_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question: "Is this an apostrophe or dash/punctuation question?",
|
||||
yesLabel: "Apostrophe (') question",
|
||||
noLabel: "Em dash (—) or other punctuation",
|
||||
yes: APOSTROPHE_SUBTREE,
|
||||
no: DASH_SUBTREE,
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"The researchers findings, which had been collected over a decade, were finally published.",
|
||||
tree: DASH_APOSTROPHE_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence: "Each student submitted there essay before the deadline.",
|
||||
tree: DASH_APOSTROPHE_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"The committee — composed of three senior researchers agreed to delay the vote.",
|
||||
tree: DASH_APOSTROPHE_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWDashesApostrophesLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Punctuation 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">
|
||||
Dashes, Apostrophes & More
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how punctuation structures meaning — then master every SAT trap.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Em Dash Pairs",
|
||||
desc: "Use two em dashes to set off nonessential info in the middle of a sentence — one to open, one to close.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Apostrophe: Possession",
|
||||
desc: "Add 's to singular nouns; add ' only to plurals ending in -s (students').",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "It's vs. Its",
|
||||
desc: "it's = it is (contraction). its = possessive. Test by substituting 'it is.'",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Confused Words",
|
||||
desc: "their (possessive), there (place), they're (contraction). who's (who is), whose (possessive).",
|
||||
},
|
||||
].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>
|
||||
|
||||
{/* Common Traps */}
|
||||
<div className="space-y-3 mb-8">
|
||||
{[
|
||||
{
|
||||
label: "Its vs. It's",
|
||||
desc: "'its' is possessive (like 'his/her'). 'it's' = it is. Test: substitute 'it is' — if it works, use it's.",
|
||||
},
|
||||
{
|
||||
label: "Unpaired Em Dash",
|
||||
desc: "Em dashes around a nonessential phrase MUST be paired. One dash in the middle of a sentence is always wrong.",
|
||||
},
|
||||
{
|
||||
label: "Their / There / They're",
|
||||
desc: "'their' = possessive, 'there' = place or existence, 'they're' = they are. The SAT uses all three as traps.",
|
||||
},
|
||||
].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>
|
||||
|
||||
{/* 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">
|
||||
On the SAT, every apostrophe is either a contraction or possession
|
||||
— never both. Test contractions by substitution. Test possessives
|
||||
by checking what owns what.
|
||||
</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: "Mismatched Dash & Comma",
|
||||
desc: "Opening with a dash and closing with a comma (or vice versa) is always wrong. Em dashes must pair with em dashes.",
|
||||
},
|
||||
{
|
||||
label: "it's / its Confusion",
|
||||
desc: "Test: can you replace it with 'it is'? If yes → it's. If no → its. This is the #1 apostrophe trap on the SAT.",
|
||||
},
|
||||
{
|
||||
label: "Confused Homophones",
|
||||
desc: "their/there/they're and whose/who's are the most common wrong-word traps. Always ask: possession, place, or contraction?",
|
||||
},
|
||||
].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(4, 6).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{BOUNDARIES_MEDIUM.slice(2, 3).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 EBRWDashesApostrophesLesson;
|
||||
328
src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx
Normal file
328
src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx
Normal file
@ -0,0 +1,328 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
COMMAND_EVIDENCE_EASY,
|
||||
COMMAND_EVIDENCE_MEDIUM,
|
||||
} from "../../../data/rw/command-of-evidence";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"According to the passage, what was the primary reason early astronomers miscalculated the distance to the Moon?",
|
||||
passage: [
|
||||
"Early astronomers were remarkably accurate in measuring celestial positions using basic instruments.",
|
||||
"However, calculating the actual distances to celestial bodies proved far more difficult.",
|
||||
"The primary obstacle was the lack of reliable baseline measurements on Earth's surface — without knowing the exact distance between two observation points, triangulation calculations were fundamentally flawed.",
|
||||
"It was not until the 18th century that land surveys became precise enough to enable accurate parallax measurements.",
|
||||
"Even then, the results varied significantly depending on atmospheric conditions during observation.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 explicitly states the primary reason: lack of reliable baseline measurements on Earth made triangulation calculations fundamentally flawed. This is directly stated — no inference needed. The correct answer to an explicit meaning question will mirror this sentence's claim.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"What does the author explicitly say about the role of microorganisms in soil health?",
|
||||
passage: [
|
||||
"Healthy agricultural soil is far more than a medium for plant roots.",
|
||||
"It is a living system teeming with billions of microorganisms per teaspoon.",
|
||||
"These microorganisms — bacteria, fungi, and protozoa — break down organic matter, releasing nutrients that plants need to grow.",
|
||||
"When farmers apply excess synthetic fertilizers, these microbial communities are disrupted, often permanently.",
|
||||
"Over time, this leads to soil compaction and loss of the natural nutrient cycle that sustained agriculture for millennia.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 explicitly states the role: microorganisms break down organic matter and release nutrients for plants. This is directly stated, word for word. Sentence 4 is about what harms them, and sentence 5 is about consequences — neither is the answer to what their role IS.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"According to the passage, when did scientists first observe the phenomenon described?",
|
||||
passage: [
|
||||
"The bioluminescence of deep-sea creatures has fascinated marine biologists for over a century.",
|
||||
"Early expeditions in the late 1800s brought up specimens that glowed faintly in darkness, confounding researchers.",
|
||||
"It was during the 1977 Alvin submersible dives off the Galápagos Rift that scientists first observed bioluminescence in its natural habitat.",
|
||||
"The footage captured was grainy but unmistakable — entire communities of organisms pulsing with cold blue light.",
|
||||
"This discovery fundamentally changed scientists' understanding of life in oxygen-deprived environments.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
'Sentence 3 explicitly states when: during the 1977 Alvin submersible dives. This is the direct answer. Sentence 2 mentions earlier expeditions but notes researchers were "confounded" — they observed specimens but not the phenomenon in its natural habitat.',
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWExplicitMeaningLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-teal-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-teal-600 text-white" : isPast ? "bg-teal-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-teal-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="Concept & Annotation"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Concept & Annotation */}
|
||||
<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-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Information & Ideas
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Explicit Meaning
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
"According to the passage" = the answer is IN the text. No
|
||||
inference. No outside knowledge. Find it, paraphrase it, pick it.
|
||||
</p>
|
||||
|
||||
{/* Rule grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: "1",
|
||||
title: "Locate, Don't Interpret",
|
||||
body: "For explicit meaning questions, the answer is stated directly in the passage. Don't paraphrase or infer.",
|
||||
},
|
||||
{
|
||||
num: "2",
|
||||
title: "Line Reference = Go There",
|
||||
body: "If the question gives a line or paragraph, read 2 lines above AND below the cited text for full context.",
|
||||
},
|
||||
{
|
||||
num: "3",
|
||||
title: "Match the Exact Claim",
|
||||
body: "The correct answer mirrors what the text says, using different words. Avoid choices that add, subtract, or distort.",
|
||||
},
|
||||
{
|
||||
num: "4",
|
||||
title: "Best Evidence Pairs",
|
||||
body: "On DSAT, many explicit questions ask you to select BOTH the answer AND the quote that proves it. They must match.",
|
||||
},
|
||||
].map((rule) => (
|
||||
<div
|
||||
key={rule.num}
|
||||
className="rounded-2xl border border-teal-200 bg-teal-50 p-5"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="w-7 h-7 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{rule.num}
|
||||
</span>
|
||||
<p className="text-sm font-bold text-teal-900">
|
||||
{rule.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
{rule.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Static annotation visual */}
|
||||
<h3 className="text-lg font-bold text-slate-800 mb-3">
|
||||
How to Annotate an Explicit Question
|
||||
</h3>
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="rounded-xl bg-teal-100 border border-teal-200 p-4">
|
||||
<p className="text-xs font-bold text-teal-700 uppercase tracking-wider mb-1">
|
||||
Question type
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"According to the passage, the researcher concluded that..."
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-green-100 border border-green-200 p-4">
|
||||
<p className="text-xs font-bold text-green-700 uppercase tracking-wider mb-1">
|
||||
Strategy
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
Find the paragraph containing the researcher's conclusion. Read
|
||||
it directly.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-orange-100 border border-orange-200 p-4">
|
||||
<p className="text-xs font-bold text-orange-700 uppercase tracking-wider mb-1">
|
||||
Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
A choice that is TRUE but not stated in the passage — it's an
|
||||
inference, not explicit meaning.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trap callout */}
|
||||
<div className="rounded-2xl bg-red-50 border border-red-200 p-5 mb-8">
|
||||
<p className="text-sm font-bold text-red-800 mb-2">
|
||||
The #1 Explicit Meaning Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
An answer that is logically consistent with the passage but not
|
||||
actually stated. If you can't point to the exact sentence that
|
||||
proves it, it's not explicit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden rule */}
|
||||
<div className="rounded-2xl bg-teal-900 p-6 mb-8">
|
||||
<p className="text-xs font-bold text-teal-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-white font-semibold leading-relaxed">
|
||||
Explicit = you can put your finger on the line. If you have to
|
||||
reason from multiple pieces, it's an inference question.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Evidence Hunter{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Evidence Hunter */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Interactive Practice
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Read each passage and click the sentence that directly answers the
|
||||
question. The answer must be explicitly stated — no reasoning
|
||||
required.
|
||||
</p>
|
||||
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{COMMAND_EVIDENCE_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
{COMMAND_EVIDENCE_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-teal-900 text-white font-bold rounded-full hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWExplicitMeaningLesson;
|
||||
316
src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx
Normal file
316
src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx
Normal file
@ -0,0 +1,316 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
RHETORICAL_EASY,
|
||||
RHETORICAL_MEDIUM,
|
||||
} from "../../../data/rw/rhetorical-synthesis";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"Goal: Support the claim that urban trees improve air quality. Which note most directly accomplishes this goal?",
|
||||
passage: [
|
||||
"Urban trees provide shade, reducing heat absorption by buildings and roads.",
|
||||
"A single mature tree absorbs up to 48 pounds of CO\u2082 per year and filters particulate matter from the air.",
|
||||
"Tree-lined streets have been linked to higher property values in multiple U.S. cities.",
|
||||
"Urban tree canopies reduce storm water runoff by intercepting rainfall before it reaches drains.",
|
||||
"Studies show that access to green spaces reduces stress and improves mental health outcomes.",
|
||||
],
|
||||
evidenceIndex: 1,
|
||||
explanation:
|
||||
"Note 2 is the only one that directly addresses AIR QUALITY — specifically CO\u2082 absorption and filtering of particulate matter. Notes 1, 3, 4, and 5 address heat, property values, water, and mental health respectively. The goal specifies air quality, so only note 2 directly accomplishes it. This mirrors how SAT synthesis questions work: identify the goal, then find the note that MATCHES it.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Goal: Emphasize the economic cost of food waste. Which sentence best accomplishes this goal using the notes below?",
|
||||
passage: [
|
||||
"Approximately one-third of all food produced globally is wasted each year.",
|
||||
"Food waste occurs at every stage: production, processing, retail, and consumption.",
|
||||
"In the United States alone, food waste costs the economy an estimated $218 billion annually.",
|
||||
"Landfill decomposition of food waste produces methane, a potent greenhouse gas.",
|
||||
"Reducing household food waste by 50% could save the average American family approximately $1,500 per year.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 directly addresses the economic cost of food waste with a specific figure ($218 billion). The goal specifies ECONOMIC COST, not scale (sentence 1), process (sentence 2), environmental impact (sentence 4), or household savings (sentence 5). Sentence 5 mentions savings but focuses on consumers — sentence 3 is the broadest economic cost figure and best accomplishes the goal.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Goal: Contrast how A. cerana and A. mellifera differ in learning speed. Which sentence best accomplishes this goal?",
|
||||
passage: [
|
||||
"Biologist Keiko Tanaka studied two bee species in a controlled laboratory setting.",
|
||||
"The study measured how quickly each species associated a scent with a sugar reward.",
|
||||
"A. cerana reached the learning criterion after an average of 8 trials.",
|
||||
"A. mellifera required an average of 12 trials to reach the same criterion.",
|
||||
"Tanaka hypothesized that differences in foraging environments may explain the variation.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
'To contrast the two species\' learning speeds, the answer must include BOTH data points and explicitly compare them. Sentence 3 alone only gives A. cerana\'s data. The ideal answer would use sentences 3 and 4 together: "8 trials vs. 12 trials." But if choosing a single sentence that begins the contrast, sentence 3 establishes A. cerana\'s faster rate (8 < 12). On the SAT, the correct synthesis choice typically combines both relevant notes into one sentence — watch for answer choices that include "whereas," "while," or "compared to."',
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWExpressionIdeasLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-fuchsia-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-fuchsia-600 text-white" : isPast ? "bg-fuchsia-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-fuchsia-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="Synthesis & Goals" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Synthesis & Goals */}
|
||||
<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-fuchsia-100 text-fuchsia-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Expression of Ideas
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Synthesis & Goals
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
You are given student notes and a stated goal. Choose the sentence
|
||||
that uses the most relevant notes to accomplish exactly that goal —
|
||||
nothing more, nothing less.
|
||||
</p>
|
||||
|
||||
{/* Rule Grid */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-fuchsia-50 border border-fuchsia-200 space-y-5">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Four Rules for Synthesis Questions
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{[
|
||||
{
|
||||
title: "Read the Goal First",
|
||||
body: "Always read the student's goal BEFORE the notes. The goal tells you what the correct sentence must accomplish.",
|
||||
},
|
||||
{
|
||||
title: "Match All Note Bullets",
|
||||
body: "The correct answer uses only information from the provided notes — no outside knowledge, no unsupported claims.",
|
||||
},
|
||||
{
|
||||
title: "Goal-Relevant Details Only",
|
||||
body: "If the goal is 'to emphasize the benefit,' an answer mentioning cost is wrong even if the cost data exists in the notes.",
|
||||
},
|
||||
{
|
||||
title: "Avoid Unsupported Additions",
|
||||
body: "If an answer adds a causal claim ('because of X') not supported by the notes, it's wrong.",
|
||||
},
|
||||
].map((rule, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white rounded-xl p-4 border border-fuchsia-100"
|
||||
>
|
||||
<p className="text-sm font-bold text-fuchsia-800 mb-1">
|
||||
{rule.title}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
{rule.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Static annotation visual */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-fuchsia-50 border border-fuchsia-200 space-y-3">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900 mb-1">
|
||||
Goal vs. Data: Worked Example
|
||||
</h3>
|
||||
<div className="bg-fuchsia-100 rounded-xl p-4 border border-fuchsia-200">
|
||||
<p className="text-xs font-bold text-fuchsia-800 mb-1">
|
||||
Notes bullet:
|
||||
</p>
|
||||
<p className="text-sm text-fuchsia-900">
|
||||
"Average temperature drop: 1.5°C in areas with green roofs"
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-100 rounded-xl p-4 border border-green-200">
|
||||
<p className="text-xs font-bold text-green-800 mb-1">
|
||||
Goal: Emphasize the cooling benefit
|
||||
</p>
|
||||
<p className="text-sm text-green-900">
|
||||
Use: "Neighborhoods with cool roofs are up to 1.5°C cooler."
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 rounded-xl p-4 border border-orange-200">
|
||||
<p className="text-xs font-bold text-orange-800 mb-1">
|
||||
Wrong answer:
|
||||
</p>
|
||||
<p className="text-sm text-orange-900">
|
||||
"Cool roofs are cheaper than conventional roofs" — not supported
|
||||
by the temperature note, and mentions cost (irrelevant to goal).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trap callout */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-5 mb-8">
|
||||
<p className="text-sm font-bold text-red-800 mb-1">
|
||||
Synthesis Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
An answer that uses data from the notes but addresses the WRONG
|
||||
goal. Always check that your answer accomplishes what the student
|
||||
asked — not just that it mentions the right topic.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden Rule */}
|
||||
<div className="bg-fuchsia-900 rounded-2xl p-6 mb-8">
|
||||
<p className="text-xs font-bold text-fuchsia-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-base font-bold text-white">
|
||||
Goal → Data match. The correct sentence must (1) accomplish the
|
||||
stated goal, (2) use only note data, and (3) not add unsupported
|
||||
claims.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-fuchsia-600 font-bold hover:text-fuchsia-800 transition-colors"
|
||||
>
|
||||
Next: Evidence Hunter{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Evidence Hunter */}
|
||||
<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">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Each passage is a set of student notes. Read the stated goal, then
|
||||
select the sentence that best accomplishes it. Reveal the
|
||||
explanation to check your reasoning.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-fuchsia-600 font-bold hover:text-fuchsia-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{RHETORICAL_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="rose" />
|
||||
))}
|
||||
{RHETORICAL_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="rose" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-fuchsia-900 text-white font-bold rounded-full hover:bg-fuchsia-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWExpressionIdeasLesson;
|
||||
516
src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx
Normal file
516
src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx
Normal file
@ -0,0 +1,516 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import DataClaimWidget, {
|
||||
type DataExercise,
|
||||
} from "../../../components/lessons/DataClaimWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
COMMAND_EVIDENCE_EASY,
|
||||
COMMAND_EVIDENCE_MEDIUM,
|
||||
} from "../../../data/rw/command-of-evidence";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const DATA_EXERCISES: DataExercise[] = [
|
||||
{
|
||||
title: "Bar — School Activities",
|
||||
chart: {
|
||||
type: "bar",
|
||||
title:
|
||||
"Student Participation in Extracurricular Activities (% of students)",
|
||||
yLabel: "% of students",
|
||||
xLabel: "Grade Level",
|
||||
unit: "%",
|
||||
source: "School District Survey, 2023",
|
||||
series: [
|
||||
{
|
||||
name: "Sports",
|
||||
data: [
|
||||
{ label: "Gr 9", value: 45 },
|
||||
{ label: "Gr 10", value: 42 },
|
||||
{ label: "Gr 11", value: 38 },
|
||||
{ label: "Gr 12", value: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Arts & Music",
|
||||
data: [
|
||||
{ label: "Gr 9", value: 28 },
|
||||
{ label: "Gr 10", value: 30 },
|
||||
{ label: "Gr 11", value: 32 },
|
||||
{ label: "Gr 12", value: 35 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Academic Clubs",
|
||||
data: [
|
||||
{ label: "Gr 9", value: 18 },
|
||||
{ label: "Gr 10", value: 22 },
|
||||
{ label: "Gr 11", value: 26 },
|
||||
{ label: "Gr 12", value: 31 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "Sports participation declines as students advance from Grade 9 to Grade 12.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"The chart shows sports falling from 45% (Grade 9) to 30% (Grade 12) — a consistent decrease at every grade level. This is directly supported.",
|
||||
},
|
||||
{
|
||||
text: "Students drop sports because they find academic clubs more interesting.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The chart shows participation trends but gives no information about WHY students make these choices. Reasons (interest, time, pressure) cannot be inferred from percentages alone.",
|
||||
},
|
||||
{
|
||||
text: "Sports is the most popular activity among students in every grade level shown.",
|
||||
verdict: "contradicted",
|
||||
explanation:
|
||||
"Sports leads in Grades 9–11, but in Grade 12 Arts & Music (35%) exceeds Sports (30%). Since Sports is NOT the top activity in Grade 12, this claim is directly contradicted by the data.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Line — Temperature",
|
||||
chart: {
|
||||
type: "line",
|
||||
title: "Average Global Temperature Anomaly (°C above 1951–1980 baseline)",
|
||||
yLabel: "Anomaly (°C)",
|
||||
xLabel: "Year",
|
||||
unit: "°C",
|
||||
source: "NASA GISS Surface Temperature Analysis",
|
||||
series: [
|
||||
{
|
||||
name: "Temperature Anomaly",
|
||||
data: [
|
||||
{ label: "1980", value: 0.26 },
|
||||
{ label: "1990", value: 0.44 },
|
||||
{ label: "2000", value: 0.42 },
|
||||
{ label: "2005", value: 0.67 },
|
||||
{ label: "2010", value: 0.72 },
|
||||
{ label: "2015", value: 0.87 },
|
||||
{ label: "2020", value: 1.02 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "The temperature anomaly in 2020 was higher than in any previous year shown in the graph.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"+1.02°C in 2020 is the highest data point — every prior year is lower. This is directly supported by the graph.",
|
||||
},
|
||||
{
|
||||
text: "The temperature anomaly decreased between 1990 and 2000.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"The graph shows +0.44°C in 1990 and +0.42°C in 2000 — a slight downward dip. This is directly supported, though the change is small.",
|
||||
},
|
||||
{
|
||||
text: "Human industrial activity is the primary cause of the temperature increases shown.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The graph records temperature trends but provides no data on causes. Attributing increases to human activity requires additional scientific evidence not present in this graph.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Bar — Media Use",
|
||||
chart: {
|
||||
type: "bar",
|
||||
title: "Average Daily Media Consumption by Age Group (hours per day)",
|
||||
yLabel: "Hours per day",
|
||||
xLabel: "Media Type",
|
||||
unit: " hr",
|
||||
source: "National Media Survey, 2024",
|
||||
series: [
|
||||
{
|
||||
name: "Ages 13–17",
|
||||
data: [
|
||||
{ label: "Social Media", value: 4.8 },
|
||||
{ label: "Streaming", value: 3.2 },
|
||||
{ label: "Video Games", value: 2.5 },
|
||||
{ label: "Reading", value: 0.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Ages 18–24",
|
||||
data: [
|
||||
{ label: "Social Media", value: 3.6 },
|
||||
{ label: "Streaming", value: 3.9 },
|
||||
{ label: "Video Games", value: 1.8 },
|
||||
{ label: "Reading", value: 1.1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "Teenagers (ages 13–17) spend more time on social media than on any other media type shown.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"Social Media (4.8 hr) is the highest value for ages 13–17, exceeding Streaming (3.2 hr), Video Games (2.5 hr), and Reading (0.7 hr). Directly supported.",
|
||||
},
|
||||
{
|
||||
text: "Adults ages 18–24 spend more time on streaming video than teenagers do.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"Ages 18–24: Streaming = 3.9 hr. Ages 13–17: Streaming = 3.2 hr. 3.9 > 3.2, so this claim is directly supported.",
|
||||
},
|
||||
{
|
||||
text: "Teenagers read less because social media is more entertaining to them.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The chart shows that teens spend little time reading (0.7 hr), but provides no data about why. Entertainment preferences require separate survey data not shown here.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Line — Renewable Energy",
|
||||
chart: {
|
||||
type: "line",
|
||||
title: "Renewable Energy Share of U.S. Electricity Generation (%)",
|
||||
yLabel: "% of generation",
|
||||
xLabel: "Year",
|
||||
unit: "%",
|
||||
source: "U.S. Energy Information Administration",
|
||||
series: [
|
||||
{
|
||||
name: "Solar",
|
||||
data: [
|
||||
{ label: "2010", value: 0.1 },
|
||||
{ label: "2013", value: 0.4 },
|
||||
{ label: "2016", value: 1.3 },
|
||||
{ label: "2019", value: 2.7 },
|
||||
{ label: "2022", value: 5.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Wind",
|
||||
data: [
|
||||
{ label: "2010", value: 2.3 },
|
||||
{ label: "2013", value: 4.1 },
|
||||
{ label: "2016", value: 5.6 },
|
||||
{ label: "2019", value: 7.3 },
|
||||
{ label: "2022", value: 10.2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "Wind energy generated a larger share of electricity than solar energy in every year shown.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"In all five years (2010–2022), Wind exceeds Solar: e.g., 2010 (2.3% vs. 0.1%) and 2022 (10.2% vs. 5.5%). This trend is directly visible throughout the graph.",
|
||||
},
|
||||
{
|
||||
text: "Solar energy's share grew by more percentage points than wind energy's share between 2010 and 2022.",
|
||||
verdict: "contradicted",
|
||||
explanation:
|
||||
"Solar: 0.1% → 5.5% = +5.4 points. Wind: 2.3% → 10.2% = +7.9 points. Wind grew by more absolute percentage points, so the claim that Solar grew more is directly contradicted.",
|
||||
},
|
||||
{
|
||||
text: "Renewable energy will replace all fossil fuels within the next 20 years.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The graph shows growth trends for two renewables through 2022 but provides no data about total energy mix, future rates, or fossil fuel usage. Future predictions require additional data not shown here.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWGraphicDisplaysLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-amber-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-amber-600 text-white" : isPast ? "bg-amber-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-amber-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="Reading Graphics" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Data Claim Lab"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* SECTION 0: Reading Graphics */}
|
||||
<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-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Graphic Displays
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Reading Graphics
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Every SAT graphic question tests the same skill: does the data
|
||||
directly prove the claim? Master these four rules before anything
|
||||
else.
|
||||
</p>
|
||||
|
||||
{/* Rule Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
<div className="rounded-2xl p-6 bg-amber-50 border border-amber-200 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-7 h-7 rounded-full bg-amber-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
1
|
||||
</span>
|
||||
<p className="text-sm font-bold text-amber-900">
|
||||
Read the Title First
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
The title tells you what variable is being measured, the time
|
||||
frame, and the units. Read it before looking at data points.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl p-6 bg-amber-50 border border-amber-200 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-7 h-7 rounded-full bg-amber-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
2
|
||||
</span>
|
||||
<p className="text-sm font-bold text-amber-900">
|
||||
Identify Axes and Units
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
X-axis (horizontal) = independent variable (often time or
|
||||
category). Y-axis (vertical) = measured value. Always check
|
||||
units (%, count, dollars, etc.).
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl p-6 bg-amber-50 border border-amber-200 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-7 h-7 rounded-full bg-amber-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
3
|
||||
</span>
|
||||
<p className="text-sm font-bold text-amber-900">
|
||||
Look for Trends, Not Just Values
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
SAT questions often ask about overall trends
|
||||
(increasing/decreasing) or comparisons between categories, not
|
||||
single data points.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl p-6 bg-amber-50 border border-amber-200 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-7 h-7 rounded-full bg-amber-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
4
|
||||
</span>
|
||||
<p className="text-sm font-bold text-amber-900">
|
||||
The Claim Must Match the Data Exactly
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
A claim is 'supported' only if the data directly proves it.
|
||||
'Contradicted' if the data proves the opposite. 'Neither' if the
|
||||
data is irrelevant or insufficient.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Static Annotation Visual */}
|
||||
<div className="rounded-2xl p-6 mb-6 bg-amber-50 border border-amber-200 space-y-4">
|
||||
<h3 className="text-base font-bold text-amber-900 mb-2">
|
||||
How to Evaluate a Claim Against a Graph
|
||||
</h3>
|
||||
<div className="rounded-xl p-4 bg-amber-100 border border-amber-200">
|
||||
<p className="text-xs font-bold text-amber-800 uppercase tracking-wider mb-1">
|
||||
Graph Context
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
Graph title: "Annual Carbon Emissions by Sector (2000–2020)"
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl p-4 bg-green-100 border border-green-200">
|
||||
<p className="text-xs font-bold text-green-800 uppercase tracking-wider mb-1">
|
||||
Supported Claim
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"Transportation emissions increased between 2000 and 2020" — if
|
||||
the line goes up, this is directly proven.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl p-4 bg-orange-100 border border-orange-200">
|
||||
<p className="text-xs font-bold text-orange-800 uppercase tracking-wider mb-1">
|
||||
Neither-Proven Claim
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"Electric vehicles caused the transportation increase" — the
|
||||
graph shows the trend, not the cause. Causation requires
|
||||
additional data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trap Callout */}
|
||||
<div className="rounded-2xl p-5 mb-6 bg-red-50 border border-red-200">
|
||||
<p className="text-xs font-bold text-red-700 uppercase tracking-wider mb-2">
|
||||
Common Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
The biggest graphic display trap: a claim that is{" "}
|
||||
<span className="font-bold text-red-800">PLAUSIBLE</span> and
|
||||
consistent with real-world knowledge but is{" "}
|
||||
<span className="font-bold text-red-800">
|
||||
NOT proven by this specific graph
|
||||
</span>
|
||||
. The graph must be sufficient to prove or disprove the claim on
|
||||
its own.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden Rule */}
|
||||
<div className="rounded-2xl p-5 mb-8 bg-amber-900 text-white">
|
||||
<p className="text-xs font-bold text-amber-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed">
|
||||
Only use what's on the graphic. Outside knowledge, reasonable
|
||||
assumptions, and probable causes are all off-limits. The data
|
||||
either proves it, disproves it, or doesn't address it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Data Claim Lab{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* SECTION 1: Data Claim Lab */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Interactive Practice
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Data Claim Lab
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Read each dataset, then judge whether each claim is supported,
|
||||
contradicted, or neither proven by the data.
|
||||
</p>
|
||||
|
||||
<DataClaimWidget exercises={DATA_EXERCISES} accentColor="amber" />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* SECTION 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{COMMAND_EVIDENCE_EASY.slice(2, 4).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
{COMMAND_EVIDENCE_MEDIUM.slice(1, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-amber-900 text-white font-bold rounded-full hover:bg-amber-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWGraphicDisplaysLesson;
|
||||
329
src/pages/student/lessons/EBRWInferencesLesson.tsx
Normal file
329
src/pages/student/lessons/EBRWInferencesLesson.tsx
Normal file
@ -0,0 +1,329 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
INFERENCES_EASY,
|
||||
INFERENCES_MEDIUM,
|
||||
} from "../../../data/rw/inferences";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"What can be inferred from this passage about the long-term effects of the policy?",
|
||||
passage: [
|
||||
"When the city introduced congestion pricing in 2019, many business owners predicted economic disaster.",
|
||||
"Three years later, traffic in the city center had declined by 28%, and air quality had measurably improved.",
|
||||
"Revenue from the pricing scheme was reinvested in public transit, increasing bus and metro frequency by 40%.",
|
||||
"Business revenues in the city center rose by an average of 12% over the same period, contradicting earlier fears.",
|
||||
"Several other major cities are now closely studying the program as a potential model.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
"Sentence 4 most strongly supports the inference that the policy had positive long-term effects on the local economy — directly contradicting predictions of economic harm. This is a valid inference because it follows necessarily from the evidence. Sentence 5 supports the inference that the policy was considered successful, but sentence 4 specifically addresses the economic outcome.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"What does the passage imply about the relationship between diet and cognitive decline?",
|
||||
passage: [
|
||||
"Alzheimer's disease affects more than 55 million people worldwide.",
|
||||
"In recent years, researchers have shifted focus from genetic factors alone to lifestyle factors, including diet.",
|
||||
"Several large-scale studies have found that individuals who follow Mediterranean-style diets — rich in vegetables, fish, and olive oil — show slower rates of cognitive decline.",
|
||||
"However, researchers caution that correlation does not establish causation, and no single food has been proven to prevent Alzheimer's.",
|
||||
"Still, the evidence is strong enough that many neurologists now discuss dietary patterns with patients at risk.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
'Sentence 3 most directly supports the implication: Mediterranean diets are associated with slower cognitive decline. This is an inference the passage clearly supports. Sentence 4 is a caution about causation — it limits the inference, which is exactly why "diet prevents Alzheimer\'s" (too strong) would be wrong.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"What can be inferred about the scientist's attitude toward the technology?",
|
||||
passage: [
|
||||
"Dr. Reyes has spent the last decade studying CRISPR applications in agriculture.",
|
||||
"In her 2023 report, she called the technology 'one of the most significant breakthroughs in food science in the last fifty years.'",
|
||||
"She was careful, however, to note that large-scale deployment would require extensive safety testing over multiple growing seasons.",
|
||||
"She also advocated for transparent public communication about how modified crops differ from conventional ones.",
|
||||
"Despite her caution, her lab has continued to accelerate its own research timeline.",
|
||||
],
|
||||
evidenceIndex: 1,
|
||||
explanation:
|
||||
"Sentence 2 most directly reveals the scientist's attitude: she views CRISPR as one of the most significant breakthroughs in fifty years — clearly positive. The word \"careful\" in sentence 3 adds nuance but doesn't change her fundamental enthusiasm. An inference about her attitude should be grounded in her own words in sentence 2.",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWInferencesLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-teal-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-teal-600 text-white" : isPast ? "bg-teal-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-teal-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="Concept & Annotation"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Concept & Annotation */}
|
||||
<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-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Information & Ideas
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Inferences
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
A valid inference is not stated but is strongly supported. It never
|
||||
exceeds what the text supports — and never uses extreme language.
|
||||
</p>
|
||||
|
||||
{/* Rule grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: "1",
|
||||
title: "Inference = Logical Extension",
|
||||
body: "An inference is not stated directly. It's a conclusion that must logically follow from what the text says.",
|
||||
},
|
||||
{
|
||||
num: "2",
|
||||
title: "Stay Close to the Text",
|
||||
body: "The SAT rewards inferences that are a small, necessary step from the evidence. Avoid dramatic leaps.",
|
||||
},
|
||||
{
|
||||
num: "3",
|
||||
title: "Supported, Not Proven",
|
||||
body: "A valid inference is supported by the text but not explicitly stated. It must be consistent with ALL of the passage, not just one line.",
|
||||
},
|
||||
{
|
||||
num: "4",
|
||||
title: "Eliminate Extreme Language",
|
||||
body: "Inferences with 'always,' 'never,' 'all,' 'none,' 'impossible' are almost always wrong — the passage rarely proves absolutes.",
|
||||
},
|
||||
].map((rule) => (
|
||||
<div
|
||||
key={rule.num}
|
||||
className="rounded-2xl border border-teal-200 bg-teal-50 p-5"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="w-7 h-7 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{rule.num}
|
||||
</span>
|
||||
<p className="text-sm font-bold text-teal-900">
|
||||
{rule.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
{rule.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Static annotation visual */}
|
||||
<h3 className="text-lg font-bold text-slate-800 mb-3">
|
||||
Valid vs. Invalid Inference — Annotated
|
||||
</h3>
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="rounded-xl bg-teal-100 border border-teal-200 p-4">
|
||||
<p className="text-xs font-bold text-teal-700 uppercase tracking-wider mb-1">
|
||||
Stated in the text
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"The researcher found that sleep-deprived students scored 15%
|
||||
lower on memory tests."
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-green-100 border border-green-200 p-4">
|
||||
<p className="text-xs font-bold text-green-700 uppercase tracking-wider mb-1">
|
||||
Valid inference
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
Sleep deprivation negatively affects memory performance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-orange-100 border border-orange-200 p-4">
|
||||
<p className="text-xs font-bold text-orange-700 uppercase tracking-wider mb-1">
|
||||
Invalid inference
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"Sleep is the most important factor in academic performance." —
|
||||
Too broad, not proven by one study.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trap callout */}
|
||||
<div className="rounded-2xl bg-red-50 border border-red-200 p-5 mb-8">
|
||||
<p className="text-sm font-bold text-red-800 mb-2">
|
||||
Inference Trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
A choice that is plausible in real life but goes BEYOND what the
|
||||
passage can actually support. Always ask: "Can I prove this using
|
||||
only what the passage says?"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden rule */}
|
||||
<div className="rounded-2xl bg-teal-900 p-6 mb-8">
|
||||
<p className="text-xs font-bold text-teal-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-white font-semibold leading-relaxed">
|
||||
Inferences are the smallest logical step the text allows. If the
|
||||
inference requires outside knowledge or an additional assumption,
|
||||
it's wrong.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Evidence Hunter{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Evidence Hunter */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Interactive Practice
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
For each passage, click the sentence that most strongly supports the
|
||||
inference asked. Think: which sentence does the most work for this
|
||||
conclusion?
|
||||
</p>
|
||||
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{INFERENCES_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
{INFERENCES_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-teal-900 text-white font-bold rounded-full hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWInferencesLesson;
|
||||
347
src/pages/student/lessons/EBRWMainIdeaLesson.tsx
Normal file
347
src/pages/student/lessons/EBRWMainIdeaLesson.tsx
Normal file
@ -0,0 +1,347 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
CENTRAL_IDEAS_EASY,
|
||||
CENTRAL_IDEAS_MEDIUM,
|
||||
} from "../../../data/rw/central-ideas-details";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question: "Which sentence states the central claim of this passage?",
|
||||
passage: [
|
||||
"Urban heat islands are a well-documented phenomenon in modern cities.",
|
||||
"Researchers have found that cities are, on average, 1–3°C warmer than surrounding rural areas.",
|
||||
"The primary driver of this warming is the replacement of natural surfaces with concrete, asphalt, and buildings, which absorb and retain heat.",
|
||||
"However, urban tree canopies and green roofs have been shown to significantly reduce surface temperatures.",
|
||||
"With targeted investment in urban greening programs, cities can meaningfully counteract the heat island effect and improve quality of life for residents.",
|
||||
],
|
||||
evidenceIndex: 4,
|
||||
explanation:
|
||||
"Sentence 5 is the central claim — it states the author's full position: that urban greening can counteract the heat island effect. Sentences 1–3 are context and causes; sentence 4 is a supporting detail. The main idea must capture the author's complete argument, not just the topic or a single detail.",
|
||||
},
|
||||
{
|
||||
question: "What is the main idea of this passage about memory research?",
|
||||
passage: [
|
||||
"For decades, scientists believed that long-term memories were fixed once formed.",
|
||||
"New research challenges this view, showing that memories are reconstructed each time they are recalled.",
|
||||
"During recall, memories become temporarily unstable — a process called reconsolidation.",
|
||||
"One study found that introducing false information during reconsolidation could alter participants' memories.",
|
||||
"These findings suggest that human memory is not a static record but a dynamic, reconstructable process — with significant implications for eyewitness testimony in courts.",
|
||||
],
|
||||
evidenceIndex: 1,
|
||||
explanation:
|
||||
"Sentence 2 states the main idea: memory research has overturned the old view, showing memories are reconstructed rather than fixed. Sentence 5 expands the implication, but sentence 2 states the core claim. On the SAT, the main idea often appears early in the passage as the thesis — watch for sentences that challenge conventional wisdom.",
|
||||
},
|
||||
{
|
||||
question: "Which sentence best captures what the entire passage argues?",
|
||||
passage: [
|
||||
"Microplastics have been detected in virtually every environment on Earth, from mountain peaks to deep ocean trenches.",
|
||||
"Scientists discovered microplastics in Antarctic ice as early as 2014.",
|
||||
"Recent studies have found microplastic particles in human blood, lung tissue, and breast milk.",
|
||||
"While the health effects remain under investigation, preliminary evidence links microplastic exposure to inflammation and hormonal disruption.",
|
||||
"Given their pervasive presence and potential health consequences, microplastics demand urgent regulatory attention and research investment.",
|
||||
],
|
||||
evidenceIndex: 4,
|
||||
explanation:
|
||||
"Sentence 5 is the main idea — it synthesizes the evidence (pervasive presence + health concerns) into a policy argument (urgent action needed). Sentences 1–4 all provide supporting evidence for this central claim. The main idea in argumentative passages typically appears as the author's call to action or judgment.",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWMainIdeaLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-teal-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-teal-600 text-white" : isPast ? "bg-teal-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-teal-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="Concept & Annotation"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* ── SECTION 0: Concept & Annotation ── */}
|
||||
<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-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4 self-start">
|
||||
Information & Ideas
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Main Idea
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-10">
|
||||
The main idea is what the author is arguing about the topic — not
|
||||
just the topic itself. One sentence that covers everything.
|
||||
</p>
|
||||
|
||||
{/* Rule Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10">
|
||||
{/* Rule 1 */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-2xl p-5">
|
||||
<p className="text-xs font-bold text-teal-600 uppercase tracking-wider mb-2">
|
||||
Rule 1 — Topic vs. Main Idea
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-bold">Topic</span> = what the passage is
|
||||
about (1–2 words). <span className="font-bold">Main Idea</span>{" "}
|
||||
= what the author <span className="italic">says</span> about the
|
||||
topic (full claim).
|
||||
</p>
|
||||
</div>
|
||||
{/* Rule 2 */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-2xl p-5">
|
||||
<p className="text-xs font-bold text-teal-600 uppercase tracking-wider mb-2">
|
||||
Rule 2 — One-Sentence Test
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
The main idea fits in one sentence that covers the{" "}
|
||||
<span className="font-bold">WHOLE</span> passage — not just one
|
||||
paragraph.
|
||||
</p>
|
||||
</div>
|
||||
{/* Rule 3 */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-2xl p-5">
|
||||
<p className="text-xs font-bold text-teal-600 uppercase tracking-wider mb-2">
|
||||
Rule 3 — Scope Check
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-bold">Too narrow</span> = describes only
|
||||
one detail. <span className="font-bold">Too broad</span> =
|
||||
overgeneralizes beyond what the text says.
|
||||
</p>
|
||||
</div>
|
||||
{/* Rule 4 */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-2xl p-5">
|
||||
<p className="text-xs font-bold text-teal-600 uppercase tracking-wider mb-2">
|
||||
Rule 4 — Author's Stance
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
For persuasive texts, the main idea includes the author's{" "}
|
||||
<span className="font-bold">position</span>, not just the topic.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Annotated Passage Visual */}
|
||||
<div className="rounded-2xl border border-teal-200 overflow-hidden mb-8 shadow-sm">
|
||||
<div className="bg-teal-600 px-5 py-3">
|
||||
<p className="text-xs font-bold text-white uppercase tracking-wider">
|
||||
Identifying Parts of a Passage
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 space-y-4">
|
||||
{/* Box 1: Topic */}
|
||||
<div className="bg-teal-100 border border-teal-200 rounded-xl p-4">
|
||||
<p className="text-xs font-bold text-teal-700 uppercase tracking-wider mb-1">
|
||||
Topic
|
||||
</p>
|
||||
<p className="text-sm text-slate-800 font-medium">
|
||||
Climate change and coral reefs
|
||||
</p>
|
||||
<p className="text-xs text-teal-600 mt-1">
|
||||
1–2 words — the subject, not the argument.
|
||||
</p>
|
||||
</div>
|
||||
{/* Box 2: Main Idea */}
|
||||
<div className="bg-green-100 border border-green-200 rounded-xl p-4">
|
||||
<p className="text-xs font-bold text-green-700 uppercase tracking-wider mb-1">
|
||||
Main Idea
|
||||
</p>
|
||||
<p className="text-sm text-slate-800 font-medium">
|
||||
Coral reefs face existential threats from climate change, but
|
||||
targeted conservation efforts can slow their decline.
|
||||
</p>
|
||||
<p className="text-xs text-green-700 mt-1">
|
||||
The author's full claim — covers the entire passage.
|
||||
</p>
|
||||
</div>
|
||||
{/* Box 3: Detail */}
|
||||
<div className="bg-orange-100 border border-orange-200 rounded-xl p-4">
|
||||
<p className="text-xs font-bold text-orange-700 uppercase tracking-wider mb-1">
|
||||
Supporting Detail
|
||||
</p>
|
||||
<p className="text-sm text-slate-800">
|
||||
"In the Great Barrier Reef, coral cover has declined by 50%
|
||||
since 1985."
|
||||
</p>
|
||||
<p className="text-xs text-orange-700 mt-1">
|
||||
← supporting evidence, NOT the main idea
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SAT Trap Callout */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-2xl p-5 mb-6">
|
||||
<p className="text-xs font-bold text-red-700 uppercase tracking-wider mb-2">
|
||||
SAT Trap to Watch For
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
The SAT will offer a choice that is{" "}
|
||||
<span className="font-bold">TRUE but too narrow</span> — it
|
||||
describes only one paragraph's detail, not the whole passage's
|
||||
claim. Always ask: does this cover the{" "}
|
||||
<span className="font-bold">ENTIRE passage</span>?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Golden Rule */}
|
||||
<div className="bg-teal-900 rounded-2xl p-5 mb-10">
|
||||
<p className="text-xs font-bold text-teal-300 uppercase tracking-wider mb-2">
|
||||
Golden Rule
|
||||
</p>
|
||||
<p className="text-sm text-white leading-relaxed">
|
||||
The main idea is the one sentence the author would give if you
|
||||
asked:{" "}
|
||||
<span className="italic">
|
||||
"What's the point of this entire piece?"
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Evidence Hunter{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 1: Evidence Hunter ── */}
|
||||
<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">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Read each passage and tap the sentence that best states the central
|
||||
claim. Think before you click.
|
||||
</p>
|
||||
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-teal-600 font-bold hover:text-teal-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{CENTRAL_IDEAS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
{CENTRAL_IDEAS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-teal-900 text-white font-bold rounded-full hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWMainIdeaLesson;
|
||||
430
src/pages/student/lessons/EBRWPronounsLesson.tsx
Normal file
430
src/pages/student/lessons/EBRWPronounsLesson.tsx
Normal file
@ -0,0 +1,430 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
FORM_STRUCTURE_EASY,
|
||||
FORM_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/form-structure-sense";
|
||||
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: "Pronoun\u2013Antecedent Agreement",
|
||||
segments: [
|
||||
{ text: "Each student", type: "subject", label: "Antecedent: singular" },
|
||||
{ text: " must submit", type: "verb", label: "Verb" },
|
||||
{
|
||||
text: " their",
|
||||
type: "conjunction",
|
||||
label: "\u26a0 Incorrect: \u2018their\u2019 is plural",
|
||||
},
|
||||
{ text: " own assignment", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Who vs. Whom",
|
||||
segments: [
|
||||
{
|
||||
text: "The scientist",
|
||||
type: "subject",
|
||||
label: "Subject of main clause",
|
||||
},
|
||||
{
|
||||
text: " who",
|
||||
type: "conjunction",
|
||||
label: "Subject pronoun: \u2018who\u2019 replaces \u2018he/she\u2019",
|
||||
},
|
||||
{ text: " won the award", type: "ic", label: "Relative clause" },
|
||||
{ text: " is my mentor", type: "verb", label: "Main verb" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Pronoun Case: Subject vs. Object",
|
||||
segments: [
|
||||
{ text: "The award was given to", type: "ic", label: "" },
|
||||
{
|
||||
text: " her",
|
||||
type: "conjunction",
|
||||
label:
|
||||
"Object pronoun: \u2018her\u2019 after preposition \u2018to\u2019",
|
||||
},
|
||||
{ text: " and", type: "conjunction", label: "" },
|
||||
{
|
||||
text: " me",
|
||||
type: "conjunction",
|
||||
label:
|
||||
"Object pronoun: \u2018me\u2019 not \u2018I\u2019 \u2014 object position",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const PRONOUN_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question: "What type of pronoun problem is this?",
|
||||
hint: "Is the question about: (1) who/whom, (2) they/it agreement with antecedent, or (3) subject vs. object case (I/me, he/him, she/her)?",
|
||||
yesLabel: "who / whom choice",
|
||||
noLabel: "Agreement or case question",
|
||||
yes: {
|
||||
id: "who-whom",
|
||||
question:
|
||||
"Is the pronoun acting as the SUBJECT of its clause (performing the action)?",
|
||||
hint: "Substitute: if 'he/she' fits \u2192 use 'who'. If 'him/her' fits \u2192 use 'whom'. Try: 'The scientist who/whom won\u2026' \u2192 'he won' \u2192 'who'.",
|
||||
yesLabel: "Yes \u2014 it's the subject (he/she would fit)",
|
||||
noLabel: "No \u2014 it's the object (him/her would fit)",
|
||||
yes: {
|
||||
id: "use-who",
|
||||
result:
|
||||
"\u2713 Use 'who' \u2014 it's in subject position (replaces he/she).",
|
||||
resultType: "correct",
|
||||
ruleRef: "who = he/she | whom = him/her",
|
||||
},
|
||||
no: {
|
||||
id: "use-whom",
|
||||
result:
|
||||
"\u2713 Use 'whom' \u2014 it's in object position (replaces him/her).",
|
||||
resultType: "correct",
|
||||
ruleRef: "whom = him/her | 'To whom' = 'To him'",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "agreement-or-case",
|
||||
question:
|
||||
"Does the pronoun need to AGREE with its antecedent (match in number)?",
|
||||
hint: "Antecedent = the noun the pronoun refers back to. Example: 'Each student submitted their assignment' \u2014 'their' must match 'each student' (singular).",
|
||||
yesLabel: "Yes \u2014 agreement question",
|
||||
noLabel: "No \u2014 subject/object case question (I vs. me)",
|
||||
yes: {
|
||||
id: "agreement",
|
||||
question:
|
||||
"Is the antecedent singular or an indefinite pronoun (each, every, anyone, someone)?",
|
||||
yesLabel: "Yes \u2014 singular or indefinite",
|
||||
noLabel: "No \u2014 clearly plural",
|
||||
yes: {
|
||||
id: "singular-antecedent",
|
||||
result:
|
||||
"\u26a0 Use a SINGULAR pronoun: 'he or she' / 'his or her'. Indefinite pronouns (each, someone, anyone) require singular pronouns. 'Their' is plural and incorrect here.",
|
||||
resultType: "warning",
|
||||
ruleRef: "Each student \u2192 his or her (not their)",
|
||||
},
|
||||
no: {
|
||||
id: "plural-antecedent",
|
||||
result:
|
||||
"\u2713 Use a plural pronoun: they, their, them \u2014 matches the plural antecedent.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Plural noun] \u2192 they/their/them",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "case",
|
||||
question:
|
||||
"Is the pronoun in SUBJECT position (before the verb, doing the action)?",
|
||||
hint: "Try covering the other person: 'He and I went' \u2192 'I went' \u2713. 'Between he and I' \u2192 'Between I' \u2717 \u2192 use 'me'.",
|
||||
yesLabel: "Yes \u2014 it's the subject",
|
||||
noLabel: "No \u2014 it's after a preposition or is an object",
|
||||
yes: {
|
||||
id: "subject-case",
|
||||
result: "\u2713 Use subject pronouns: I, he, she, we, they, who.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Subject: I/he/she/we/they performed the action",
|
||||
},
|
||||
no: {
|
||||
id: "object-case",
|
||||
result:
|
||||
"\u2713 Use object pronouns: me, him, her, us, them, whom \u2014 after prepositions or as direct/indirect objects.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Object: gave it to me/him/her/them",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"Each of the students submitted their assignments before the deadline.",
|
||||
tree: PRONOUN_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"The award was presented to Sarah and I at the end of the ceremony.",
|
||||
tree: PRONOUN_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"The professor, who students found inspiring, won the teaching award.",
|
||||
tree: PRONOUN_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWPronounsLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Pronoun 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">
|
||||
Pronouns & Agreement
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how pronoun choices work — then master the rules the SAT tests
|
||||
most.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Who vs. Whom",
|
||||
desc: "who = subject (he/she). whom = object (him/her). Test by substituting.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Pronoun\u2013Antecedent Agreement",
|
||||
desc: "Pronoun must match antecedent in number. Each/anyone/someone \u2192 singular.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Subject Pronouns",
|
||||
desc: "I, he, she, we, they, who \u2014 before verbs as subjects.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Object Pronouns",
|
||||
desc: "me, him, her, us, them, whom \u2014 after prepositions or as objects.",
|
||||
},
|
||||
].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">
|
||||
Pronoun 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"
|
||||
/>
|
||||
|
||||
{/* Common Traps */}
|
||||
<div className="space-y-3 mt-8 mb-6">
|
||||
{[
|
||||
{
|
||||
label: "Each/Every/Anyone \u2192 Singular Pronoun",
|
||||
desc: "'Each student submitted their assignment' sounds natural but is wrong. Use 'his or her' for formal SAT writing.",
|
||||
},
|
||||
{
|
||||
label: "I vs. Me After Prepositions",
|
||||
desc: "'between you and I' is always wrong. After prepositions, always use object pronouns: me, him, her, us, them.",
|
||||
},
|
||||
{
|
||||
label: "Who vs. Whom \u2014 Substitute Test",
|
||||
desc: "Replace who/whom with he/him. If 'he' fits \u2192 who. If 'him' fits \u2192 whom.",
|
||||
},
|
||||
].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>
|
||||
|
||||
<div className="mt-2 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">
|
||||
The SAT's most common pronoun traps are 'their' with singular
|
||||
antecedents, 'I' after prepositions, and 'who' where 'whom' is
|
||||
correct. Test with substitution: he/him for who/whom, 'it is' for
|
||||
'it's'.
|
||||
</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 pronoun logic one question at a time. Click your
|
||||
answer at each step.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
{FORM_STRUCTURE_EASY.slice(2, 4).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{FORM_STRUCTURE_MEDIUM.slice(1, 2).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 EBRWPronounsLesson;
|
||||
411
src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx
Normal file
411
src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx
Normal file
@ -0,0 +1,411 @@
|
||||
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: "Semicolon — Joining Two ICs",
|
||||
segments: [
|
||||
{
|
||||
text: "The experiment was a success",
|
||||
type: "ic",
|
||||
label: "Independent Clause",
|
||||
},
|
||||
{ text: ";", type: "punct" },
|
||||
{ text: " the team celebrated", type: "ic", label: "Independent Clause" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Semicolon + Conjunctive Adverb",
|
||||
segments: [
|
||||
{
|
||||
text: "The data was incomplete",
|
||||
type: "ic",
|
||||
label: "Independent Clause",
|
||||
},
|
||||
{ text: ";", type: "punct" },
|
||||
{ text: " however", type: "conjunction", label: "Conjunctive Adverb" },
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the conclusions remained valid",
|
||||
type: "ic",
|
||||
label: "Independent Clause",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Colon — Introduction",
|
||||
segments: [
|
||||
{
|
||||
text: "The study revealed one key finding",
|
||||
type: "ic",
|
||||
label: "Complete Sentence",
|
||||
},
|
||||
{ text: ":", type: "punct" },
|
||||
{
|
||||
text: " memory improves with spaced repetition",
|
||||
type: "ic",
|
||||
label: "What the colon introduces",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const TREE_ROOT: TreeNode = {
|
||||
id: "root",
|
||||
question: "Is this a SEMICOLON or COLON question?",
|
||||
hint: "Look at the punctuation options: does the question ask about ; or :?",
|
||||
yesLabel: "Semicolon (;)",
|
||||
noLabel: "Colon (:)",
|
||||
yes: {
|
||||
id: "semicolon",
|
||||
question:
|
||||
"Is the word AFTER the semicolon a conjunctive adverb (however, therefore, moreover, thus, consequently, furthermore)?",
|
||||
hint: "These are NOT FANBOYS conjunctions. They look like transitions.",
|
||||
yesLabel: "Yes — conjunctive adverb follows",
|
||||
noLabel: "No — regular sentence follows",
|
||||
yes: {
|
||||
id: "semi-conjunctive",
|
||||
result:
|
||||
"✓ Correct use! Semicolon before the conjunctive adverb, comma after it.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[IC]; [conjunctive adverb], [IC]",
|
||||
},
|
||||
no: {
|
||||
id: "semi-no-conj",
|
||||
question: "Can BOTH sides stand alone as complete sentences?",
|
||||
yesLabel: "Yes — both are complete sentences",
|
||||
noLabel: "No — one side is incomplete",
|
||||
yes: {
|
||||
id: "semi-both-ic",
|
||||
result: "✓ Correct! A semicolon joins two related independent clauses.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[IC]; [IC]",
|
||||
},
|
||||
no: {
|
||||
id: "semi-fragment",
|
||||
result:
|
||||
"⚠ Error! A semicolon requires COMPLETE sentences on both sides. If one side is a phrase or clause, the semicolon is wrong.",
|
||||
resultType: "warning",
|
||||
ruleRef: "Both sides must be independent clauses",
|
||||
},
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "colon",
|
||||
question: "Is the part BEFORE the colon a complete sentence on its own?",
|
||||
hint: "Cover everything after the colon. Does what's left make a complete sentence?",
|
||||
yesLabel: "Yes — complete sentence before colon",
|
||||
noLabel: "No — incomplete phrase before colon",
|
||||
yes: {
|
||||
id: "colon-complete",
|
||||
result:
|
||||
"✓ Correct! The colon introduces what follows (list, explanation, or quotation).",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Complete sentence]: [list / explanation / quotation]",
|
||||
},
|
||||
no: {
|
||||
id: "colon-fragment",
|
||||
result:
|
||||
'⚠ Error! The part before a colon must ALWAYS be a complete sentence. Never use a colon after an incomplete phrase like "The results included:".',
|
||||
resultType: "warning",
|
||||
ruleRef:
|
||||
"[Complete sentence]: [explanation] — left side must be complete",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"The experiment yielded surprising results however the team chose not to revise their hypothesis.",
|
||||
tree: TREE_ROOT,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"The professor outlined three requirements for the assignment a thesis, supporting evidence, and a conclusion.",
|
||||
tree: TREE_ROOT,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"The researchers disagreed; one believed the effect was temporary, the other argued it was permanent.",
|
||||
tree: TREE_ROOT,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWSemicolonsColonsLesson: 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.ElementType;
|
||||
}) => {
|
||||
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">
|
||||
Semicolons & Colons
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Two marks — very different jobs. Master both to avoid the #1
|
||||
punctuation error on the SAT.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Semicolon between ICs",
|
||||
desc: "Use a semicolon to join two independent clauses without a conjunction. Both sides must be complete sentences.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Semicolon + Conjunctive Adverb",
|
||||
desc: "Use a semicolon BEFORE conjunctive adverbs (however, therefore, moreover, consequently). They are NOT FANBOYS.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Colon for Explanation/List",
|
||||
desc: "Use a colon when the left side is a complete sentence that introduces a list, explanation, or quotation. Never colon if left side is incomplete.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "No comma for conjunctive adverbs",
|
||||
desc: 'Never use a comma alone before "however," "therefore," etc. That creates a comma splice.',
|
||||
},
|
||||
].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 semicolon requires a <em>complete sentence on both sides</em>. A
|
||||
colon requires a <em>complete sentence on the left</em> only.
|
||||
Conjunctive adverbs like "however" and "therefore" are NOT FANBOYS
|
||||
— they always need a semicolon before them, not a comma.
|
||||
</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: "Conjunctive Adverb Trap",
|
||||
desc: 'Students confuse "however," "therefore," etc. with FANBOYS. These words need a semicolon before them, not a comma. "The data was good, however, we needed more" is a comma splice.',
|
||||
},
|
||||
{
|
||||
label: "Colon After Incomplete Phrase",
|
||||
desc: 'Never: "The categories include: A, B, C." The word "include" makes it incomplete. Correct: "There are three categories: A, B, C."',
|
||||
},
|
||||
{
|
||||
label: "Two Semicolons in One Clause",
|
||||
desc: "If the question has answer choices with extra semicolons in the middle of a clause, they are always wrong.",
|
||||
},
|
||||
].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(2, 4).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{BOUNDARIES_MEDIUM.slice(1, 2).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 EBRWSemicolonsColonsLesson;
|
||||
438
src/pages/student/lessons/EBRWSentenceStructureLesson.tsx
Normal file
438
src/pages/student/lessons/EBRWSentenceStructureLesson.tsx
Normal file
@ -0,0 +1,438 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
FORM_STRUCTURE_EASY,
|
||||
FORM_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/form-structure-sense";
|
||||
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: "Parallel Structure — List",
|
||||
segments: [
|
||||
{
|
||||
text: "The program requires",
|
||||
type: "subject",
|
||||
label: "Subject + Verb",
|
||||
},
|
||||
{ text: " attending lectures", type: "conjunction", label: "Gerund: ✓" },
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " completing assignments",
|
||||
type: "conjunction",
|
||||
label: "Gerund: ✓",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{ text: " and", type: "conjunction", label: "FANBOYS" },
|
||||
{
|
||||
text: " passing the final exam",
|
||||
type: "conjunction",
|
||||
label: "Gerund: ✓ — all match",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Misplaced Modifier",
|
||||
segments: [
|
||||
{
|
||||
text: "Running through the park",
|
||||
type: "modifier",
|
||||
label: "Modifier: describes who is running",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the sunset",
|
||||
type: "subject",
|
||||
label: "⚠ 'The sunset' can't run — modifier is misplaced!",
|
||||
},
|
||||
{ text: " was beautiful", type: "verb", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Dangling Modifier — Fixed",
|
||||
segments: [
|
||||
{
|
||||
text: "Running through the park",
|
||||
type: "modifier",
|
||||
label: "Modifier: describes who is running",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " she",
|
||||
type: "subject",
|
||||
label: "✓ 'She' is the one running — modifier now matches",
|
||||
},
|
||||
{ text: " watched the sunset", type: "verb", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const STRUCTURE_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question: "Is this a PARALLELISM or MODIFIER problem?",
|
||||
hint: "Parallelism: a list where elements don't match in form. Modifier: a descriptive phrase that's attached to the wrong noun.",
|
||||
yesLabel: "Parallelism — list items don't match",
|
||||
noLabel: "Modifier — descriptive phrase in wrong place",
|
||||
yes: {
|
||||
id: "parallelism",
|
||||
question: "What grammatical form do MOST items in the list use?",
|
||||
hint: "Identify the form of the majority: nouns? gerunds (-ing)? infinitives (to-)? adjectives?",
|
||||
yesLabel: "Gerunds (-ing forms) dominate",
|
||||
noLabel: "Nouns, infinitives, or adjectives dominate",
|
||||
yes: {
|
||||
id: "parallel-gerunds",
|
||||
result:
|
||||
"✓ Convert ALL list items to gerunds (-ing). 'The program requires attending, completing, and passing' — all match.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[verb]-ing, [verb]-ing, and [verb]-ing",
|
||||
},
|
||||
no: {
|
||||
id: "parallel-other",
|
||||
question: "Are the items a mix of infinitives (to-verb) and other forms?",
|
||||
yesLabel: "Yes — some 'to-verb', some don't match",
|
||||
noLabel: "No — nouns or adjectives are mixed",
|
||||
yes: {
|
||||
id: "parallel-infinitives",
|
||||
result:
|
||||
"✓ Convert ALL list items to infinitives (to + verb): 'to attend, to complete, and to pass.'",
|
||||
resultType: "correct",
|
||||
ruleRef: "to [verb], to [verb], and to [verb]",
|
||||
},
|
||||
no: {
|
||||
id: "parallel-nouns",
|
||||
result:
|
||||
"✓ Make all items the same part of speech — all nouns, all adjectives, or all the same structure. The parallelism rule: match the form of the first item.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[noun], [noun], and [noun] — all same form",
|
||||
},
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "modifier",
|
||||
question:
|
||||
"Is the modifier at the BEGINNING of the sentence (before the comma)?",
|
||||
hint: "Opening modifiers like 'Running through the park,' must be immediately followed by the noun/pronoun they describe.",
|
||||
yesLabel: "Yes — opens the sentence before a comma",
|
||||
noLabel: "No — modifier is elsewhere in the sentence",
|
||||
yes: {
|
||||
id: "opening-modifier",
|
||||
question:
|
||||
"Does the word immediately AFTER the comma refer to who/what is doing the action in the modifier?",
|
||||
yesLabel: "Yes — it matches",
|
||||
noLabel: "No — it doesn't make sense (dangling modifier)",
|
||||
yes: {
|
||||
id: "modifier-correct",
|
||||
result:
|
||||
"✓ The modifier is correctly placed. It immediately precedes the noun it describes.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Modifier phrase], [the noun it describes] [verb]...",
|
||||
},
|
||||
no: {
|
||||
id: "dangling-modifier",
|
||||
result:
|
||||
"⚠ Dangling modifier! The noun after the comma must be the one performing the action in the phrase. Fix: rewrite so the correct subject follows the comma.",
|
||||
resultType: "warning",
|
||||
ruleRef: "Fix: '[Modifier], [WHO is doing it] [verb]'",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "mid-sentence-modifier",
|
||||
result:
|
||||
"⚠ Misplaced modifier! The modifier should be placed immediately next to what it describes. Move it closer to the word it modifies.",
|
||||
resultType: "warning",
|
||||
ruleRef: "Place modifier directly next to the word it describes",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"The program requires attending lectures, to complete assignments, and passing the final.",
|
||||
tree: STRUCTURE_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"Having reviewed all the evidence, a verdict was reached by the jury.",
|
||||
tree: STRUCTURE_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence: "She enjoys hiking, to read, and cooking on weekends.",
|
||||
tree: STRUCTURE_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWSentenceStructureLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Structure 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">
|
||||
Sentence Structure
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how sentences are built — then learn exactly where parallelism
|
||||
breaks and modifiers go wrong.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Parallelism in Lists",
|
||||
desc: "All list items must match: all gerunds, all infinitives, all nouns, etc.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Parallel Comparisons",
|
||||
desc: "'more X than Y' — X and Y must be the same type (comparing like to like).",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Opening Modifier Rule",
|
||||
desc: "The noun immediately after a comma must be what the modifier describes.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Squinting Modifiers",
|
||||
desc: "A modifier must be placed unambiguously next to what it modifies.",
|
||||
},
|
||||
].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"
|
||||
/>
|
||||
|
||||
{/* SAT Traps */}
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-8 mb-4">
|
||||
Common SAT Traps
|
||||
</h3>
|
||||
<div className="space-y-3 mb-8">
|
||||
{[
|
||||
{
|
||||
label: "Subtle Parallelism Break",
|
||||
desc: "'researching, to analyze, and write' — the break ('to analyze') looks acceptable but breaks the gerund pattern.",
|
||||
},
|
||||
{
|
||||
label: "Dangling Modifier",
|
||||
desc: "'Having reviewed the evidence, a verdict was reached' — the verdict didn't review anything. The jury did.",
|
||||
},
|
||||
{
|
||||
label: "Squinting Modifier",
|
||||
desc: "A modifier placed between two clauses so it's unclear which one it modifies.",
|
||||
},
|
||||
].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>
|
||||
|
||||
<div className="mt-2 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">
|
||||
Parallel structure and modifiers both follow one principle: every
|
||||
part of a sentence must connect clearly and consistently to what
|
||||
it describes or lists. Mismatch in form or placement = SAT error.
|
||||
</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>
|
||||
|
||||
<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>
|
||||
{FORM_STRUCTURE_EASY.slice(6, 8).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{FORM_STRUCTURE_MEDIUM.slice(3, 4).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 EBRWSentenceStructureLesson;
|
||||
424
src/pages/student/lessons/EBRWSubjectVerbLesson.tsx
Normal file
424
src/pages/student/lessons/EBRWSubjectVerbLesson.tsx
Normal file
@ -0,0 +1,424 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
FORM_STRUCTURE_EASY,
|
||||
FORM_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/form-structure-sense";
|
||||
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: "Prepositional Phrase Trap",
|
||||
segments: [
|
||||
{
|
||||
text: "The results",
|
||||
type: "subject",
|
||||
label: "True Subject: 'results' (plural)",
|
||||
},
|
||||
{
|
||||
text: " of the study",
|
||||
type: "modifier",
|
||||
label: "Prepositional Phrase — ignore for agreement",
|
||||
},
|
||||
{ text: " were", type: "verb", label: "Plural Verb ✓" },
|
||||
{ text: " significant", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Collective Noun — Singular",
|
||||
segments: [
|
||||
{
|
||||
text: "The committee",
|
||||
type: "subject",
|
||||
label: "Collective Noun — singular",
|
||||
},
|
||||
{ text: " has", type: "verb", label: "Singular Verb ✓" },
|
||||
{ text: " reached", type: "verb", label: "" },
|
||||
{ text: " a decision", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Inverted Sentence",
|
||||
segments: [
|
||||
{
|
||||
text: "Among the findings",
|
||||
type: "modifier",
|
||||
label: "Introductory Phrase — not the subject",
|
||||
},
|
||||
{
|
||||
text: " was",
|
||||
type: "verb",
|
||||
label: "Singular Verb — matches 'one key result'",
|
||||
},
|
||||
{
|
||||
text: " one key result",
|
||||
type: "subject",
|
||||
label: "True Subject: singular",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const SUBJECT_VERB_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question: "What is the TRUE grammatical subject of the sentence?",
|
||||
hint: "Strip away all prepositional phrases (of, in, with, by...), relative clauses, and modifiers. What's left is your true subject.",
|
||||
yesLabel: "I found it — it's singular",
|
||||
noLabel: "I found it — it's plural",
|
||||
yes: {
|
||||
id: "singular-subject",
|
||||
question:
|
||||
"Is the subject a collective noun (team, committee, group, class, family) acting as one unit?",
|
||||
yesLabel: "Yes — collective noun",
|
||||
noLabel: "No — regular singular noun",
|
||||
yes: {
|
||||
id: "collective-singular",
|
||||
result:
|
||||
'✓ Use a SINGULAR verb. Collective nouns acting as a single unit take singular verbs: "The committee has decided."',
|
||||
resultType: "correct",
|
||||
ruleRef: "[Collective noun] + singular verb (has, was, decides)",
|
||||
},
|
||||
no: {
|
||||
id: "regular-singular",
|
||||
question:
|
||||
"Is the subject an indefinite pronoun (everyone, each, either, neither, anyone, someone)?",
|
||||
yesLabel: "Yes — indefinite pronoun",
|
||||
noLabel: "No — regular noun",
|
||||
yes: {
|
||||
id: "indefinite-singular",
|
||||
result:
|
||||
"✓ Use a SINGULAR verb. Most indefinite pronouns (everyone, each, either, neither) are grammatically singular.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Everyone/Each/Either → singular verb",
|
||||
},
|
||||
no: {
|
||||
id: "regular-noun-singular",
|
||||
result: "✓ Use a SINGULAR verb: is, was, has, does, -s ending verbs.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Singular noun] + singular verb",
|
||||
},
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "plural-subject",
|
||||
question: "Is the subject a compound subject joined by 'and'?",
|
||||
yesLabel: "Yes — X and Y (compound)",
|
||||
noLabel: "No — regular plural",
|
||||
yes: {
|
||||
id: "compound-and",
|
||||
result:
|
||||
'✓ Use a PLURAL verb. "X and Y" joined with "and" is always plural: "The teacher and the student are ready."',
|
||||
resultType: "correct",
|
||||
ruleRef: "[Noun] and [noun] + plural verb",
|
||||
},
|
||||
no: {
|
||||
id: "regular-plural",
|
||||
question: "Is the subject joined by 'or' or 'nor'?",
|
||||
yesLabel: "Yes — X or/nor Y",
|
||||
noLabel: "No — just a regular plural noun",
|
||||
yes: {
|
||||
id: "or-nor",
|
||||
result:
|
||||
"⚠ 'Or/Nor' rule: the verb agrees with the CLOSER subject. \"Neither the students nor the teacher IS ready\" (IS agrees with 'teacher', the closer one).",
|
||||
resultType: "warning",
|
||||
ruleRef: "Neither A nor [B] + verb matching B (the closer subject)",
|
||||
},
|
||||
no: {
|
||||
id: "plain-plural",
|
||||
result: "✓ Use a PLURAL verb: are, were, have, do, -s removed.",
|
||||
resultType: "correct",
|
||||
ruleRef: "[Plural noun] + plural verb",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"The committee of senior researchers have decided to delay the publication.",
|
||||
tree: SUBJECT_VERB_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"Neither the students nor the professor were prepared for the final exam.",
|
||||
tree: SUBJECT_VERB_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"Among the most important discoveries of the decade was two breakthrough treatments.",
|
||||
tree: SUBJECT_VERB_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWSubjectVerbLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Agreement 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">
|
||||
Subject-Verb Agreement
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how sentences are built — then learn how to match the verb to
|
||||
the true subject.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Strip Prep Phrases",
|
||||
desc: 'Ignore "of the X, in the Y" — they hide the true subject. Find the core noun.',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Collective Nouns",
|
||||
desc: "team, group, committee, class → singular verb when acting as one unit.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Indefinite Pronouns",
|
||||
desc: "each, every, either, neither, anyone, someone → always singular.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Or / Nor Rule",
|
||||
desc: 'Verb matches the CLOSER subject: "Neither the students nor the teacher IS."',
|
||||
},
|
||||
].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">
|
||||
The SAT hides subjects behind prepositional phrases and inverted
|
||||
sentences. Always identify the true subject first — strip
|
||||
modifiers, then match the verb.
|
||||
</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 agreement logic one question at a time. Click your
|
||||
answer at each step.
|
||||
</p>
|
||||
|
||||
{/* Trap callouts */}
|
||||
<div className="space-y-3 mb-8">
|
||||
{[
|
||||
{
|
||||
label: "Prepositional Phrase Trap",
|
||||
desc: 'The results of the study [was/were]? Strip "of the study"; true subject is "results" (plural) → "were".',
|
||||
},
|
||||
{
|
||||
label: "Or/Nor Proximity Rule",
|
||||
desc: '"Neither A nor B" → verb matches B (the noun closer to the verb).',
|
||||
},
|
||||
{
|
||||
label: "Inverted Sentence",
|
||||
desc: '"There is/are..." and "Among X was/were Y" — find the true subject after the 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>
|
||||
{FORM_STRUCTURE_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{FORM_STRUCTURE_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 EBRWSubjectVerbLesson;
|
||||
429
src/pages/student/lessons/EBRWTransitionsLesson.tsx
Normal file
429
src/pages/student/lessons/EBRWTransitionsLesson.tsx
Normal file
@ -0,0 +1,429 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
TRANSITIONS_EASY,
|
||||
TRANSITIONS_MEDIUM,
|
||||
} from "../../../data/rw/transitions";
|
||||
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: "Contrast Transition",
|
||||
segments: [
|
||||
{ text: "The sample size was large", type: "ic", label: "First idea" },
|
||||
{ text: ";", type: "punct" },
|
||||
{
|
||||
text: " however",
|
||||
type: "conjunction",
|
||||
label: "Contrast: 'however' shows opposition",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the results were inconclusive",
|
||||
type: "ic",
|
||||
label: "Contrasting idea",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cause-Effect Transition",
|
||||
segments: [
|
||||
{
|
||||
text: "The experiment was repeated three times",
|
||||
type: "ic",
|
||||
label: "Cause / reason",
|
||||
},
|
||||
{ text: ";", type: "punct" },
|
||||
{
|
||||
text: " therefore",
|
||||
type: "conjunction",
|
||||
label: "Result: 'therefore' = as a result",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the team was confident in the data",
|
||||
type: "ic",
|
||||
label: "Effect / result",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Addition Transition",
|
||||
segments: [
|
||||
{
|
||||
text: "The study confirmed the main hypothesis",
|
||||
type: "ic",
|
||||
label: "First point",
|
||||
},
|
||||
{ text: ";", type: "punct" },
|
||||
{
|
||||
text: " furthermore",
|
||||
type: "conjunction",
|
||||
label: "Addition: 'furthermore' adds supporting info",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " it revealed two secondary effects",
|
||||
type: "ic",
|
||||
label: "Additional point",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const TRANSITION_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question:
|
||||
"What is the LOGICAL RELATIONSHIP between the two ideas being connected?",
|
||||
hint: "Read both sentences carefully. Does the second idea contradict the first? Follow from it? Add to it? Give an example of it?",
|
||||
yesLabel:
|
||||
"The second idea CONTRASTS or surprises (despite, however, although)",
|
||||
noLabel: "The second idea follows, adds to, or supports",
|
||||
yes: {
|
||||
id: "contrast",
|
||||
question:
|
||||
"Is the contrast a direct OPPOSITION (the opposite is true), or just a surprising or unexpected result?",
|
||||
yesLabel: "Direct opposition — completely opposite",
|
||||
noLabel: "Unexpected or surprising despite circumstances",
|
||||
yes: {
|
||||
id: "direct-contrast",
|
||||
result:
|
||||
"✓ Use a strong contrast word: however, on the other hand, in contrast, conversely, yet, but.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Strong contrast: however / in contrast / conversely",
|
||||
},
|
||||
no: {
|
||||
id: "concession",
|
||||
result:
|
||||
"✓ Use a concession/nuance word: nevertheless, nonetheless, still, even so, although X, Y.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Concession/nuance: nevertheless / nonetheless / even so",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "not-contrast",
|
||||
question:
|
||||
"Does the second sentence present a RESULT, CONSEQUENCE, or CONCLUSION from the first?",
|
||||
yesLabel: "Yes — cause and effect relationship",
|
||||
noLabel: "No — adding information, example, or sequence",
|
||||
yes: {
|
||||
id: "cause-effect",
|
||||
result:
|
||||
"✓ Use a cause-effect word: therefore, thus, consequently, as a result, hence.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Cause-effect: therefore / thus / consequently / as a result",
|
||||
},
|
||||
no: {
|
||||
id: "addition-example",
|
||||
question: "Is the second sentence an EXAMPLE that illustrates the first?",
|
||||
yesLabel: "Yes — gives a specific example",
|
||||
noLabel: "No — adds more supporting information",
|
||||
yes: {
|
||||
id: "example",
|
||||
result:
|
||||
"✓ Use an example word: for example, for instance, specifically, in particular.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Example: for example / for instance / in particular",
|
||||
},
|
||||
no: {
|
||||
id: "addition",
|
||||
result:
|
||||
"✓ Use an addition word: furthermore, moreover, in addition, additionally, also.",
|
||||
resultType: "correct",
|
||||
ruleRef:
|
||||
"Addition: furthermore / moreover / in addition / additionally",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"The researchers had conducted extensive trials. _____, the medication was approved for widespread use.",
|
||||
tree: TRANSITION_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"The new policy reduced costs significantly. _____, employee satisfaction dropped by 30%.",
|
||||
tree: TRANSITION_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"The findings align with previous research. _____, they support the theory proposed in 2018.",
|
||||
tree: TRANSITION_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWTransitionsLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Transition 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">
|
||||
Transitions & Logical Connections
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how transitions connect ideas — then learn to pick the right one
|
||||
every time.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Contrast",
|
||||
desc: "however, in contrast, on the other hand, conversely, yet — second idea OPPOSES first.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Concession",
|
||||
desc: "nevertheless, nonetheless, even so, still — unexpected result despite circumstances.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Cause-Effect",
|
||||
desc: "therefore, thus, consequently, as a result — second idea RESULTS from first.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Addition / Example",
|
||||
desc: "furthermore, moreover, additionally / for example, for instance — adds to or illustrates.",
|
||||
},
|
||||
].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">
|
||||
Transition 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"
|
||||
/>
|
||||
|
||||
{/* Trap callouts */}
|
||||
<div className="space-y-3 mt-8 mb-2">
|
||||
{[
|
||||
{
|
||||
label: "Contrast vs. Concession",
|
||||
desc: "'however' and 'nevertheless' both show contrast, but 'nevertheless' implies overcoming an obstacle. Don't use them interchangeably.",
|
||||
},
|
||||
{
|
||||
label: "Therefore ≠ Furthermore",
|
||||
desc: "'Therefore' means AS A RESULT. 'Furthermore' means IN ADDITION. These are NOT interchangeable. Test: does idea 2 follow FROM idea 1, or just add to it?",
|
||||
},
|
||||
{
|
||||
label: "Wrong Transition Tone",
|
||||
desc: "'for example' requires that the second sentence gives a specific INSTANCE of the first claim. 'Moreover' adds more evidence. Mix-up = wrong tone.",
|
||||
},
|
||||
].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>
|
||||
|
||||
<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">
|
||||
Test every transition by reading both sentences and asking: does
|
||||
idea 2 oppose, result from, add to, or illustrate idea 1? Pick the
|
||||
category first, then the specific word. Never guess transitions by
|
||||
sound — logic is everything.
|
||||
</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 transition logic one question at a time. Click your
|
||||
answer at each step.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
{TRANSITIONS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="rose" />
|
||||
))}
|
||||
{TRANSITIONS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="rose" />
|
||||
))}
|
||||
<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 EBRWTransitionsLesson;
|
||||
433
src/pages/student/lessons/EBRWVerbsLesson.tsx
Normal file
433
src/pages/student/lessons/EBRWVerbsLesson.tsx
Normal file
@ -0,0 +1,433 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
FORM_STRUCTURE_EASY,
|
||||
FORM_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/form-structure-sense";
|
||||
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: "Verb Tense — Past Sequence",
|
||||
segments: [
|
||||
{
|
||||
text: "By the time the results arrived",
|
||||
type: "dc",
|
||||
label: "Dependent Clause — earlier action",
|
||||
},
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the team had already published",
|
||||
type: "verb",
|
||||
label: "Past Perfect: action completed before another past action",
|
||||
},
|
||||
{ text: " their findings", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Subjunctive Mood",
|
||||
segments: [
|
||||
{
|
||||
text: "If the data were",
|
||||
type: "dc",
|
||||
label: "Subjunctive: 'were' not 'was' — hypothetical condition",
|
||||
},
|
||||
{ text: " more complete", type: "ic", label: "" },
|
||||
{ text: ",", type: "punct" },
|
||||
{
|
||||
text: " the conclusions would be stronger",
|
||||
type: "ic",
|
||||
label: "Main clause with 'would'",
|
||||
},
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Gerund vs. Infinitive",
|
||||
segments: [
|
||||
{ text: "The committee", type: "subject", label: "Subject" },
|
||||
{
|
||||
text: " decided",
|
||||
type: "verb",
|
||||
label: "Verb: 'decided' takes infinitive",
|
||||
},
|
||||
{
|
||||
text: " to postpone",
|
||||
type: "conjunction",
|
||||
label: "Infinitive: 'to + verb'",
|
||||
},
|
||||
{ text: " the vote", type: "ic", label: "" },
|
||||
{ text: ".", type: "punct" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Decision Tree data ─────────────────────────────────────────────────────
|
||||
const VERB_TREE: TreeNode = {
|
||||
id: "root",
|
||||
question: "What verb form problem is in this sentence?",
|
||||
hint: "Is it about: (1) tense consistency/sequence, (2) subjunctive mood (if/wish), or (3) verb form (gerund '-ing' vs. infinitive 'to-verb')?",
|
||||
yesLabel: "Tense or sequence issue",
|
||||
noLabel: "Subjunctive or verb form issue",
|
||||
yes: {
|
||||
id: "tense",
|
||||
question:
|
||||
"Does the sentence describe an action that happened BEFORE another past action?",
|
||||
hint: "'By the time X happened, Y had already...' — the earlier action needs past perfect (had + verb).",
|
||||
yesLabel: "Yes — one action preceded another",
|
||||
noLabel: "No — all actions at the same time",
|
||||
yes: {
|
||||
id: "past-perfect",
|
||||
result:
|
||||
"✓ Use PAST PERFECT (had + past participle) for the earlier action: 'had published,' 'had discovered,' 'had completed.'",
|
||||
resultType: "correct",
|
||||
ruleRef: "Earlier: had + [past participle] | Later: simple past",
|
||||
},
|
||||
no: {
|
||||
id: "tense-consistency",
|
||||
result:
|
||||
"✓ Keep tense CONSISTENT with the surrounding context. If the passage is in past tense, don't switch to present or future unnecessarily.",
|
||||
resultType: "correct",
|
||||
ruleRef: "Match the dominant tense of the passage",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "subj-or-form",
|
||||
question:
|
||||
"Does the sentence contain 'if,' 'wish,' 'as if,' or 'as though' — expressing a hypothetical or contrary-to-fact condition?",
|
||||
yesLabel: "Yes — hypothetical or wish",
|
||||
noLabel: "No — it's about gerund vs. infinitive",
|
||||
yes: {
|
||||
id: "subjunctive",
|
||||
question:
|
||||
"Is the condition clearly FALSE or hypothetical (not likely to be true)?",
|
||||
yesLabel: "Yes — clearly hypothetical",
|
||||
noLabel: "No — it could be real/possible",
|
||||
yes: {
|
||||
id: "subjunctive-were",
|
||||
result:
|
||||
"✓ Use SUBJUNCTIVE WERE (not 'was'): 'If the data were complete...' / 'I wish the study were larger.'",
|
||||
resultType: "correct",
|
||||
ruleRef:
|
||||
"Hypothetical: If [subject] were... / I wish [subject] were...",
|
||||
},
|
||||
no: {
|
||||
id: "indicative-if",
|
||||
result:
|
||||
"✓ Use indicative 'was' — the condition is possible, not hypothetical: 'If the data was incorrect, we would know by now.'",
|
||||
resultType: "correct",
|
||||
ruleRef: "Possible condition: If [subject] was...",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "gerund-infinitive",
|
||||
question:
|
||||
"Does the main verb PREFER a gerund (enjoy, avoid, consider, suggest) or an infinitive (decide, want, plan, need)?",
|
||||
hint: "Gerund (-ing): 'The team avoided making errors.' Infinitive (to-verb): 'The team decided to revise.'",
|
||||
yesLabel: "Gerund (-ing) after this verb",
|
||||
noLabel: "Infinitive (to-verb) after this verb",
|
||||
yes: {
|
||||
id: "use-gerund",
|
||||
result:
|
||||
"✓ Use the GERUND (-ing form): avoid, enjoy, consider, suggest, recommend → 'avoid making,' 'enjoy reading.'",
|
||||
resultType: "correct",
|
||||
ruleRef: "avoid/enjoy/consider + [verb]-ing",
|
||||
},
|
||||
no: {
|
||||
id: "use-infinitive",
|
||||
result:
|
||||
"✓ Use the INFINITIVE (to + verb): decide, want, need, plan, hope, agree → 'decided to publish,' 'hoped to complete.'",
|
||||
resultType: "correct",
|
||||
ruleRef: "decide/want/need/plan + to [verb]",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "Sentence 1",
|
||||
sentence:
|
||||
"By the time the paper was published, the researchers already discovered three additional variables.",
|
||||
tree: VERB_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 2",
|
||||
sentence:
|
||||
"If the sample size was larger, the study's conclusions would be more reliable.",
|
||||
tree: VERB_TREE,
|
||||
},
|
||||
{
|
||||
label: "Sentence 3",
|
||||
sentence:
|
||||
"The committee recommended to adopt the new protocol before the next review cycle.",
|
||||
tree: VERB_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Lesson component ───────────────────────────────────────────────────────
|
||||
const EBRWVerbsLesson: 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.ElementType;
|
||||
}) => {
|
||||
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="Verb 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">
|
||||
Verb Anatomy
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
See how verb tense, mood, and form work in context — then apply the
|
||||
rules.
|
||||
</p>
|
||||
|
||||
{/* Rule summary grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
rule: "Tense Consistency",
|
||||
desc: "Match the dominant tense of the passage. Don't randomly shift.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
rule: "Past Perfect",
|
||||
desc: "Use 'had + verb' for the earlier of two past events.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
rule: "Subjunctive Were",
|
||||
desc: "Hypothetical conditions: 'If [subject] were...' regardless of singular/plural.",
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
rule: "Gerund vs. Infinitive",
|
||||
desc: "Certain verbs take gerunds; others take infinitives. Memory: avoid/enjoy/consider + -ing. decide/want/plan + to-verb.",
|
||||
},
|
||||
].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"
|
||||
/>
|
||||
|
||||
{/* Traps */}
|
||||
<div className="mt-8 space-y-3 mb-8">
|
||||
{[
|
||||
{
|
||||
label: "Past Perfect vs. Simple Past",
|
||||
desc: "'had already published' (earlier) vs. 'published' (simple past). Don't confuse sequence with simultaneity.",
|
||||
},
|
||||
{
|
||||
label: "Was vs. Were (Subjunctive)",
|
||||
desc: "Hypothetical conditions use 'were' even with singular subjects: 'If she were the lead researcher...'",
|
||||
},
|
||||
{
|
||||
label: "Gerund/Infinitive Collocations",
|
||||
desc: "'suggest to do' is wrong; 'suggest doing' is correct. 'decide doing' is wrong; 'decide to do' is correct.",
|
||||
},
|
||||
].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>
|
||||
|
||||
<div className="mt-2 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">
|
||||
Three verb traps dominate the SAT: mixing tenses in a sequence,
|
||||
missing the subjunctive "were" in hypotheticals, and using the
|
||||
wrong verb form (gerund/infinitive). Always check the time
|
||||
relationship and the main verb's preference.
|
||||
</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 verb logic one question at a time. Click your
|
||||
answer at each step.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
{FORM_STRUCTURE_EASY.slice(4, 6).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="purple" />
|
||||
))}
|
||||
{FORM_STRUCTURE_MEDIUM.slice(2, 3).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 EBRWVerbsLesson;
|
||||
390
src/pages/student/lessons/EBRWVocabMeaningLesson.tsx
Normal file
390
src/pages/student/lessons/EBRWVocabMeaningLesson.tsx
Normal file
@ -0,0 +1,390 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import ContextEliminationWidget, {
|
||||
type VocabExercise,
|
||||
} from "../../../components/lessons/ContextEliminationWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
WORDS_CONTEXT_EASY,
|
||||
WORDS_CONTEXT_MEDIUM,
|
||||
} from "../../../data/rw/words-in-context";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EBRWVocabMeaningLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-rose-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-rose-600 text-white" : isPast ? "bg-rose-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-rose-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const VOCAB_EXERCISES: VocabExercise[] = [
|
||||
{
|
||||
sentence:
|
||||
"The critic's review was pointed, cutting directly to the weaknesses of the performance without any diplomatic softening.",
|
||||
word: "pointed",
|
||||
question: "As used in this sentence, 'pointed' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "having a sharp physical tip",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"This is the literal, physical meaning. In a review context, 'pointed' describes the directness of criticism, not a physical shape.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "deliberately directed and sharply critical",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Correct — 'pointed' here means incisively critical and direct. The phrase 'cutting directly to the weaknesses' confirms the metaphorical sharpness of the critique.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "polite and professionally worded",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Opposite — the sentence says 'without diplomatic softening,' meaning the review was NOT polite. Eliminate on opposite-meaning grounds.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "carefully structured and well-organized",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The sentence emphasizes directness and impact, not organization. 'Pointed' is about force, not structure.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"The administration's new policy represented a marked departure from the approach taken by its predecessor.",
|
||||
word: "marked",
|
||||
question: "As used in this sentence, 'marked' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "labeled or indicated with a visible sign",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Physical marking meaning. Here 'marked departure' is an idiom meaning notable/significant departure — no literal label is involved.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "slight and barely noticeable",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Opposite connotation — 'marked' as an adjective before a noun means striking or significant, the opposite of slight.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "clearly noticeable and significant",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Correct — 'a marked departure' means a clearly noticeable, significant change. This is the standard idiomatic meaning of 'marked' as an adjective.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "controversial and widely debated",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The sentence says nothing about controversy — it only describes how different the policy is. 'Marked' means notable, not controversial.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"Despite the economic pressures, the nonprofit remained committed to its founding mission, refusing to compromise its principles.",
|
||||
word: "compromise",
|
||||
question: "As used in this sentence, 'compromise' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "reach a mutual agreement through negotiation",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"This is the common meaning of 'compromise' as a noun/verb in negotiations. Here it's used differently — to weaken or undermine.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "weaken or undermine",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Correct — 'compromise its principles' means to weaken or betray them. This is the secondary meaning of 'compromise' as a verb: to expose to risk or damage.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "publicly disclose or reveal",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"This meaning applies to 'compromise' in security contexts (e.g., 'compromised data'). Here the context is about integrity, not disclosure.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "renegotiate or redefine",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"'Refusing to compromise' means refusing to weaken principles, not refusing to redefine them. Too specific and misses the core meaning.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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="Word Meaning Strategy"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Context Elimination"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Word Meaning Strategy */}
|
||||
<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-rose-100 text-rose-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Vocabulary in Context
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Word Meaning Strategy
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
A word can have many meanings. "Most nearly means" asks which
|
||||
meaning fits THIS sentence. Context, not the dictionary, is the
|
||||
judge.
|
||||
</p>
|
||||
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-2xl p-6 mb-6 space-y-4">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
Four Essential Rules
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
1. 'Most Nearly Means'
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
This question type asks for the word's meaning IN THIS
|
||||
CONTEXT, not its general definition. The same word can have
|
||||
different meanings in different sentences.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
2. Substitute Test
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Insert each answer choice into the sentence in place of the
|
||||
word. The correct answer will sound natural and preserve the
|
||||
sentence's meaning.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
3. Watch for Secondary Meanings
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The SAT deliberately picks words that have a common meaning
|
||||
AND a rarer contextual meaning. The correct answer is usually
|
||||
the less obvious one.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
4. Tone and Connotation
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Even if a word is technically correct, wrong connotation =
|
||||
wrong answer. 'Famous' and 'notorious' both mean well-known,
|
||||
but 'notorious' has negative connotation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="bg-rose-100 rounded-xl p-4 border border-rose-200">
|
||||
<p className="text-sm text-slate-800 italic">
|
||||
"The scientist's findings were met with considerable reservation
|
||||
by her peers."
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-100 rounded-xl p-4 border border-green-200">
|
||||
<p className="text-sm text-slate-800">
|
||||
<span className="font-bold text-green-800">
|
||||
'Reservation' here = doubt/skepticism.
|
||||
</span>{" "}
|
||||
Not a place to stay — context overrides the common meaning.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 rounded-xl p-4 border border-orange-200">
|
||||
<p className="text-sm text-slate-800">
|
||||
<span className="font-bold text-orange-800">
|
||||
Wrong choice trap:
|
||||
</span>{" "}
|
||||
'hesitation' — close but 'reservation' implies more sustained
|
||||
doubt, not momentary pause.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 mb-6">
|
||||
<p className="text-sm font-bold text-red-800 mb-1">
|
||||
The 'most nearly means' trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
The SAT picks the most common meaning of the word as a wrong
|
||||
answer choice. Always check if the common meaning fits the
|
||||
sentence — if it feels wrong in context, look for the rarer
|
||||
contextual meaning.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-rose-900 rounded-2xl p-5 mb-8">
|
||||
<p className="text-sm font-bold text-rose-100 mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-white">
|
||||
The correct answer substitutes naturally into the sentence without
|
||||
changing the author's intended meaning. Test each choice by
|
||||
reading the full sentence aloud.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Context Elimination{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Context Elimination */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-rose-100 text-rose-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Interactive Practice
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Context Elimination
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
For each question below, read the sentence and use context clues to
|
||||
eliminate wrong choices — then select the best meaning.
|
||||
</p>
|
||||
|
||||
<ContextEliminationWidget
|
||||
exercises={VOCAB_EXERCISES}
|
||||
accentColor="rose"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{WORDS_CONTEXT_EASY.slice(2, 4).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
{WORDS_CONTEXT_MEDIUM.slice(1, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-rose-900 text-white font-bold rounded-full hover:bg-rose-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWVocabMeaningLesson;
|
||||
386
src/pages/student/lessons/EBRWVocabPreciseLesson.tsx
Normal file
386
src/pages/student/lessons/EBRWVocabPreciseLesson.tsx
Normal file
@ -0,0 +1,386 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
|
||||
import ContextEliminationWidget, {
|
||||
type VocabExercise,
|
||||
} from "../../../components/lessons/ContextEliminationWidget";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
WORDS_CONTEXT_EASY,
|
||||
WORDS_CONTEXT_MEDIUM,
|
||||
} from "../../../data/rw/words-in-context";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EBRWVocabPreciseLesson: 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 = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}) => {
|
||||
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-rose-50" : "hover:bg-slate-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-rose-600 text-white" : isPast ? "bg-rose-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-rose-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const VOCAB_EXERCISES: VocabExercise[] = [
|
||||
{
|
||||
sentence:
|
||||
"The professor's explanation, though thorough, was unnecessarily prolix, causing many students to lose the thread of her argument.",
|
||||
word: "prolix",
|
||||
question: "As used in this sentence, 'prolix' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "unclear and ambiguous in meaning",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The sentence says students lost the thread — but the issue is length, not ambiguity. 'Unnecessarily' before 'prolix' signals excessive quantity, not lack of clarity.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "excessively long and wordy",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Fits perfectly — 'prolix' means tediously wordy. The 'unnecessarily' modifier and the result (losing the thread) both point to excessive length as the problem.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "overly formal and academic in tone",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Tone mismatch — the sentence criticizes the length, not the register. Students losing the thread suggests quantity, not formality.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "repetitive in its use of examples",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Too specific — 'prolix' applies to wordiness broadly, not specifically to repetitive examples.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"The committee's decision to release the report was seen as a pragmatic solution to a politically charged situation.",
|
||||
word: "pragmatic",
|
||||
question: "As used in this sentence, 'pragmatic' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "morally correct and ethically sound",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The sentence is about political feasibility, not ethics. 'Politically charged situation' signals practicality, not morality.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "financially motivated and cost-conscious",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"No financial context in the sentence — this is about political practicality, not money.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "practical and focused on real-world outcomes",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Correct — 'pragmatic' means dealing with things sensibly and realistically rather than ideally. The 'politically charged' context confirms they chose what would work, not what was ideal.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "cautious and risk-averse",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Close but too narrow — pragmatic means practical, not necessarily cautious. A pragmatic decision could be bold if it's realistic.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"Her account of the events was largely corroborated by the testimony of three independent witnesses.",
|
||||
word: "corroborated",
|
||||
question: "As used in this sentence, 'corroborated' most nearly means:",
|
||||
options: [
|
||||
{
|
||||
id: "A",
|
||||
definition: "challenged and disputed",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Opposite meaning — 'corroborated' means confirmed, not challenged. Three witnesses supporting her is positive confirmation.",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
definition: "confirmed and supported by additional evidence",
|
||||
isCorrect: true,
|
||||
elimReason:
|
||||
"Exact meaning — 'corroborate' means to strengthen or confirm with new evidence. Three witnesses providing the same account confirms her version.",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
definition: "recorded and preserved for future reference",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The sentence is about verification, not archiving. Witnesses confirm accuracy, not storage.",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
definition: "explained and clarified in detail",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Witnesses don't clarify an account — they confirm it. 'Corroborated' implies agreement with the existing account, not elaboration.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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="Vocabulary Strategy"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Context Elimination"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 0: Vocabulary Strategy */}
|
||||
<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-rose-100 text-rose-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Vocabulary in Context
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Vocabulary Strategy
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
The SAT vocabulary question is a logic test disguised as a word
|
||||
test. Use context clues to find the word that fits precisely — in
|
||||
meaning, tone, and register.
|
||||
</p>
|
||||
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-2xl p-6 mb-6 space-y-4">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
Four Essential Rules
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
1. Cover and Predict
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Before looking at answer choices, cover them, read the
|
||||
sentence, and predict what kind of word fits. Then find the
|
||||
choice that matches your prediction.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
2. Context Over Dictionary
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The SAT tests how a word is used HERE, not its most common
|
||||
meaning. 'Acute' usually means sharp, but in context may mean
|
||||
'severe.'
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
3. Precise Fit
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The right answer is not just generally correct — it fits the
|
||||
TONE and REGISTER of the sentence (formal/informal,
|
||||
positive/negative).
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-rose-100">
|
||||
<p className="text-xs font-bold text-rose-700 mb-1">
|
||||
4. Elimination
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The SAT rarely provides two obviously correct choices.
|
||||
Eliminate by tone mismatch, connotation, and register before
|
||||
picking.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="bg-rose-100 rounded-xl p-4 border border-rose-200">
|
||||
<p className="text-sm text-slate-800 italic">
|
||||
"The diplomat's remarks were remarkably ____, avoiding any
|
||||
language that could be misinterpreted."
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-100 rounded-xl p-4 border border-green-200">
|
||||
<p className="text-sm text-slate-800">
|
||||
<span className="font-bold text-green-800">Correct:</span>{" "}
|
||||
'measured' — implies careful, deliberate, restrained language.
|
||||
Fits diplomatic context.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 rounded-xl p-4 border border-orange-200">
|
||||
<p className="text-sm text-slate-800">
|
||||
<span className="font-bold text-orange-800">Trap:</span> 'quiet'
|
||||
— a synonym that misses the connotation. The diplomat wasn't
|
||||
silent; they were precise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 mb-6">
|
||||
<p className="text-sm font-bold text-red-800 mb-1">
|
||||
Vocabulary precision trap
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Choose a word that is a synonym but wrong for the CONTEXT.
|
||||
'Economical' and 'frugal' both mean thrifty, but 'frugal' has a
|
||||
slightly negative connotation — the wrong fit for praising
|
||||
someone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-rose-900 rounded-2xl p-5 mb-8">
|
||||
<p className="text-sm font-bold text-rose-100 mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-white">
|
||||
Re-read the sentence with your chosen word inserted. If it sounds
|
||||
perfect — not just acceptable — that's your answer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-4 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Context Elimination{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 1: Context Elimination */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-rose-100 text-rose-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4">
|
||||
Interactive Practice
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Context Elimination
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
For each question below, read the sentence and use context clues to
|
||||
eliminate wrong choices — then select the best meaning.
|
||||
</p>
|
||||
|
||||
<ContextEliminationWidget
|
||||
exercises={VOCAB_EXERCISES}
|
||||
accentColor="rose"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-rose-600 font-bold hover:text-rose-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice 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 Quiz
|
||||
</h2>
|
||||
{WORDS_CONTEXT_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
{WORDS_CONTEXT_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-rose-900 text-white font-bold rounded-full hover:bg-rose-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWVocabPreciseLesson;
|
||||
291
src/pages/student/lessons/EquivalentExpressionsLesson.tsx
Normal file
291
src/pages/student/lessons/EquivalentExpressionsLesson.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Layers,
|
||||
Grid3X3,
|
||||
Hash,
|
||||
Sigma,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import FactoringWidget from "../../../components/lessons/FactoringWidget";
|
||||
import RadicalWidget from "../../../components/lessons/RadicalWidget";
|
||||
import {
|
||||
EQUIV_EXPR_EASY,
|
||||
EQUIV_EXPR_MEDIUM,
|
||||
} from "../../../data/math/equivalent-expressions";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Distributive Property", icon: ArrowRight },
|
||||
{ title: "Combining Like Terms", icon: Layers },
|
||||
{ title: "Factoring Techniques", icon: Grid3X3 },
|
||||
{ title: "Special Products", icon: Hash },
|
||||
{ title: "Exponents & Radicals", icon: Sigma },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function EquivalentExpressionsLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Equivalent Expressions"
|
||||
sections={SECTIONS}
|
||||
color="violet"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Distributive Property */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Distributive Property
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The distributive property lets you multiply a factor across terms
|
||||
inside parentheses. It also works in reverse — factoring out a
|
||||
common factor.
|
||||
</p>
|
||||
<FormulaBox>a(b + c) = ab + ac</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Distribute" color="violet">
|
||||
<p>3(2x − 5) = 6x − 15</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Distribute a Negative" color="violet">
|
||||
<p>−2(x² − 4x + 1)</p>
|
||||
<p className="text-slate-500">= −2x² + 8x − 2</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
Watch for distributing negatives — a very common source of SAT
|
||||
errors. Remember: −(a − b) = −a + b, not −a − b.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Combining Like Terms */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Combining Like Terms
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Like terms</strong> have the same variable raised to the
|
||||
same power. Only the coefficients can differ. You can add or
|
||||
subtract like terms by combining their coefficients.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm mb-1">
|
||||
Like Terms ✓
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
3x² and −5x², 7xy and 2xy
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3">
|
||||
<p className="font-bold text-rose-800 text-sm mb-1">
|
||||
Unlike Terms ✗
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">3x² and 3x, 2xy and 2x</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="violet">
|
||||
<p>3x² + 5x − 2x² + x − 7</p>
|
||||
<p className="text-slate-500">= (3x² − 2x²) + (5x + x) − 7</p>
|
||||
<p className="text-slate-500">
|
||||
= <strong className="text-violet-700">x² + 6x − 7</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Factoring Techniques */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Factoring Techniques
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Factoring is the reverse of distributing. For trinomials x² + bx +
|
||||
c, find two numbers that <strong>add to b</strong> and{" "}
|
||||
<strong>multiply to c</strong>.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
x² + bx + c = (x + p)(x + q) where p + q = b and p × q = c
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Simple Trinomial" color="violet">
|
||||
<p>Factor: x² + 7x + 12</p>
|
||||
<p className="text-slate-500">Need: p + q = 7 and p × q = 12</p>
|
||||
<p className="text-slate-500">
|
||||
p = 3, q = 4 →{" "}
|
||||
<strong className="text-violet-700">(x + 3)(x + 4)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Leading Coefficient ≠ 1" color="violet">
|
||||
<p>Factor: 2x² + 5x − 3</p>
|
||||
<p className="text-slate-500">Product: 2 × (−3) = −6. Sum: 5</p>
|
||||
<p className="text-slate-500">
|
||||
Numbers: 6 and −1. Split: 2x² + 6x − x − 3
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Group: 2x(x + 3) − 1(x + 3) ={" "}
|
||||
<strong className="text-violet-700">(2x − 1)(x + 3)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<FactoringWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Special Products */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Special Products
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
These patterns appear constantly on the SAT. Memorize them!
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>a² − b² = (a + b)(a − b)</FormulaBox>
|
||||
<FormulaBox>a² + 2ab + b² = (a + b)²</FormulaBox>
|
||||
<FormulaBox>a² − 2ab + b² = (a − b)²</FormulaBox>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Difference of Squares" color="violet">
|
||||
<p>Factor: 4x² − 25</p>
|
||||
<p className="text-slate-500">
|
||||
= (2x)² − 5² ={" "}
|
||||
<strong className="text-violet-700">(2x + 5)(2x − 5)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Perfect Square Trinomial" color="violet">
|
||||
<p>Factor: x² + 10x + 25</p>
|
||||
<p className="text-slate-500">
|
||||
= x² + 2(5)(x) + 5² ={" "}
|
||||
<strong className="text-violet-700">(x + 5)²</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Exponents & Radicals */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Rational Exponents & Radicals
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Radicals and rational exponents are two ways to express the same
|
||||
thing.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>
|
||||
x<sup>1/n</sup> = ⁿ√x and x<sup>m/n</sup>{" "}
|
||||
= ⁿ√(x<sup>m</sup>)
|
||||
</FormulaBox>
|
||||
</div>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-100 text-violet-900">
|
||||
<th className="border border-violet-300 px-3 py-2 text-left font-bold">
|
||||
Rule
|
||||
</th>
|
||||
<th className="border border-violet-300 px-3 py-2 text-left font-bold">
|
||||
Formula
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">Product</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
x<sup>a</sup> × x<sup>b</sup> = x<sup>a+b</sup>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Quotient
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
x<sup>a</sup> ÷ x<sup>b</sup> = x<sup>a−b</sup>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Power of a Power
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
(x<sup>a</sup>)<sup>b</sup> = x<sup>ab</sup>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Zero Exponent
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
x⁰ = 1 (x ≠ 0)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Negative
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
x<sup>−n</sup> = 1 ÷ x<sup>n</sup>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="violet">
|
||||
<p>
|
||||
Simplify: x<sup>3/2</sup> × x<sup>1/2</sup>
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
= x<sup>(3/2 + 1/2)</sup> ={" "}
|
||||
<strong className="text-violet-700">x²</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Explore: Fractional Exponents ↔ Radicals
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Drag the sliders to see how the power (numerator) and root
|
||||
(denominator) relate.
|
||||
</p>
|
||||
<RadicalWidget />
|
||||
</div>
|
||||
|
||||
{/* Section 6: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{EQUIV_EXPR_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
{EQUIV_EXPR_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
289
src/pages/student/lessons/EvalStatisticalClaimsLesson.tsx
Normal file
289
src/pages/student/lessons/EvalStatisticalClaimsLesson.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Search,
|
||||
GitBranch,
|
||||
AlertTriangle,
|
||||
Scale,
|
||||
Layers,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import StudyDesignWidget from "../../../components/lessons/StudyDesignWidget";
|
||||
import {
|
||||
EVAL_STATS_EASY,
|
||||
EVAL_STATS_MEDIUM,
|
||||
} from "../../../data/math/evaluating-statistical-claims";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Observational vs Experimental", icon: Search },
|
||||
{ title: "Random Assignment", icon: GitBranch },
|
||||
{ title: "Confounding Variables", icon: AlertTriangle },
|
||||
{ title: "Causation vs Correlation", icon: Scale },
|
||||
{ title: "Identifying Bias", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function EvalStatisticalClaimsLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Evaluating Statistical Claims"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Observational vs Experimental Studies
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The type of study determines what conclusions you can draw.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<p className="font-bold text-blue-800 mb-1">
|
||||
Observational Study
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Researcher <strong>observes</strong> without intervening. Can
|
||||
show <strong>association</strong> only.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-800 mb-1">Experiment</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Researcher <strong>assigns treatments</strong>. Can show{" "}
|
||||
<strong>causation</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Spot the Difference" color="amber">
|
||||
<p>
|
||||
Observational: "Students who eat breakfast tend to have higher
|
||||
GPAs."
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Experiment: "Randomly assign half the students to eat breakfast for
|
||||
a month, compare GPAs."
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Only the experiment can claim breakfast causes higher GPAs.
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Random Assignment
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Random sampling</strong> determines generalizability.{" "}
|
||||
<strong>Random assignment</strong> determines causation. These are
|
||||
different concepts!
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2"></th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center font-bold">
|
||||
Random Assignment YES
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center font-bold">
|
||||
Random Assignment NO
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold bg-amber-50">
|
||||
Random Sampling YES
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-emerald-50 font-semibold text-emerald-800">
|
||||
Generalize + Cause & Effect
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-blue-50 font-semibold text-blue-800">
|
||||
Generalize only
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold bg-amber-50">
|
||||
Random Sampling NO
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-purple-50 font-semibold text-purple-800">
|
||||
Cause & Effect (this group only)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-center bg-rose-50 font-semibold text-rose-800">
|
||||
Neither
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<StudyDesignWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
Random <strong>sampling</strong> → can generalize to population.
|
||||
Random <strong>assignment</strong> → can claim cause-and-effect.
|
||||
These are independent concepts.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Confounding Variables
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>confounding variable</strong> is related to BOTH the
|
||||
explanatory and response variables, making it impossible to
|
||||
determine the true cause of an observed relationship.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Ice Cream & Drowning" color="amber">
|
||||
<p>Ice cream sales and drowning rates both increase in summer.</p>
|
||||
<p className="text-slate-500">
|
||||
Confounding variable:{" "}
|
||||
<strong className="text-amber-700">temperature / season</strong>
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Hot weather causes both — ice cream doesn't cause drowning!
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Coffee & Exercise" color="amber">
|
||||
<p>Study: coffee drinkers exercise more.</p>
|
||||
<p className="text-slate-500">
|
||||
Confounding: Maybe more energetic people both drink coffee AND
|
||||
exercise.
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Can't conclude coffee causes more exercise.
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Causation vs Correlation
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Correlation</strong> means two variables move together.{" "}
|
||||
<strong>Causation</strong> means one actually causes the other.
|
||||
Correlation alone does NOT prove causation.
|
||||
</p>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 text-sm mb-2">
|
||||
Three Requirements for Causation
|
||||
</p>
|
||||
<div className="space-y-1 text-sm text-slate-700">
|
||||
<p>1. Random assignment to treatment/control groups</p>
|
||||
<p>2. Controlled experiment (same conditions except treatment)</p>
|
||||
<p>3. Confounding variables ruled out</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
The SAT will try to trick you with answer choices that claim
|
||||
causation from observational studies. Always check: was there random
|
||||
assignment?
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Identifying Bias
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Bias systematically distorts results. Know these types:
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
Selection Bias
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Non-random sample excludes or over-represents groups
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
Response Bias
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Leading questions or social desirability affects answers
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
Nonresponse Bias
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
People who don't respond differ from those who do
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
Voluntary Response
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Only people with strong opinions participate
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Leading Question" color="amber">
|
||||
<p>"Do you agree that the new park is a waste of money?"</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Response bias — the question pushes toward "agree"
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{EVAL_STATS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{EVAL_STATS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
406
src/pages/student/lessons/LinearEq1VarLesson.tsx
Normal file
406
src/pages/student/lessons/LinearEq1VarLesson.tsx
Normal file
@ -0,0 +1,406 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Scale,
|
||||
ArrowRight,
|
||||
Hash,
|
||||
Lightbulb,
|
||||
BookOpen,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import LinearSolutionsWidget from "../../../components/lessons/LinearSolutionsWidget";
|
||||
import LiteralEquationWidget from "../../../components/lessons/LiteralEquationWidget";
|
||||
import {
|
||||
LINEAR_EQ_ONE_VAR_EASY,
|
||||
LINEAR_EQ_ONE_VAR_MEDIUM,
|
||||
} from "../../../data/math/linear-equations-one-var";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ title: "The Balance Principle", icon: Scale },
|
||||
{ title: "Multi-Step Equations", icon: ArrowRight },
|
||||
{ title: "Variables on Both Sides", icon: Hash },
|
||||
{ title: "Number of Solutions", icon: Lightbulb },
|
||||
{ title: "Word Problems", icon: Scale },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
/* ── tiny inline balance widget ── */
|
||||
const BalanceWidget = () => {
|
||||
const [left, setLeft] = useState(15);
|
||||
const [right, setRight] = useState(15);
|
||||
const [tilt, setTilt] = useState(0);
|
||||
const [msg, setMsg] = useState("Balanced");
|
||||
const apply = (v: number, side: "both" | "left") => {
|
||||
const nL = side === "both" || side === "left" ? left + v : left;
|
||||
const nR = side === "both" ? right + v : right;
|
||||
setLeft(nL);
|
||||
setRight(nR);
|
||||
setTilt(nL === nR ? 0 : nL > nR ? -12 : 12);
|
||||
setMsg(nL === nR ? "Balanced!" : "Unbalanced!");
|
||||
};
|
||||
const reset = () => {
|
||||
setLeft(15);
|
||||
setRight(15);
|
||||
setTilt(0);
|
||||
setMsg("Balanced");
|
||||
};
|
||||
return (
|
||||
<div className="glass-card rounded-2xl p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-bold text-slate-700">Algebra Balance Scale</h3>
|
||||
<button onClick={reset} className="text-slate-400 hover:text-slate-600">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative h-32 flex justify-center items-end mb-4">
|
||||
<div className="absolute bottom-0 w-0 h-0 border-l-[16px] border-l-transparent border-r-[16px] border-r-transparent border-b-[32px] border-b-slate-800" />
|
||||
<div
|
||||
className="w-52 h-1.5 bg-slate-600 absolute bottom-[32px] transition-transform duration-500"
|
||||
style={{ transform: `rotate(${tilt}deg)` }}
|
||||
>
|
||||
<div className="absolute left-0 -top-12 w-16 h-12 border-b-2 border-l border-r border-slate-300 rounded-b-lg bg-white/80 flex items-center justify-center">
|
||||
<span className="font-bold text-blue-600 text-sm">{left}</span>
|
||||
</div>
|
||||
<div className="absolute right-0 -top-12 w-16 h-12 border-b-2 border-l border-r border-slate-300 rounded-b-lg bg-white/80 flex items-center justify-center">
|
||||
<span className="font-bold text-emerald-600 text-sm">{right}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className={`text-center font-bold text-sm mb-4 ${tilt === 0 ? "text-emerald-600" : "text-rose-600"}`}
|
||||
>
|
||||
{msg}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => apply(-5, "left")}
|
||||
className="p-2.5 rounded-lg border border-slate-200 hover:border-rose-300 hover:bg-rose-50 text-xs font-bold text-slate-600 transition-all"
|
||||
>
|
||||
−5 Left ONLY
|
||||
</button>
|
||||
<button
|
||||
onClick={() => apply(-5, "both")}
|
||||
className="p-2.5 rounded-lg border-2 border-blue-400 bg-blue-50 hover:bg-blue-100 text-xs font-bold text-blue-700 transition-all"
|
||||
>
|
||||
−5 BOTH Sides
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LinearEq1VarLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Linear Equations in One Variable"
|
||||
sections={SECTIONS}
|
||||
color="blue"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* ── Section 1: Balance Principle ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
The Balance Principle
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A linear equation is like a balance scale — whatever you do to one
|
||||
side, you <strong>must</strong> do to the other. The goal is always
|
||||
to <strong>isolate the variable</strong> on one side.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
If A = B, then A + c = B + c and A × c = B × c
|
||||
</FormulaBox>
|
||||
<p className="text-slate-600 text-sm">
|
||||
This is the <strong>Addition Property</strong> and{" "}
|
||||
<strong>Multiplication Property</strong> of equality.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<BalanceWidget />
|
||||
<div className="mt-6">
|
||||
<ExampleCard title="Example: Simple Equation" color="blue">
|
||||
<p>Solve: 3x + 7 = 22</p>
|
||||
<p className="text-slate-500">Subtract 7: 3x = 15</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by 3: <strong className="text-blue-700">x = 5</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Section 2: Multi-Step ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Multi-Step Equations
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Follow the <strong>4-step process</strong> for any linear equation:
|
||||
</p>
|
||||
<div className="space-y-3 mt-3">
|
||||
{[
|
||||
{
|
||||
n: "1",
|
||||
t: "Distribute",
|
||||
d: "Expand parentheses and clear fractions by multiplying by the LCD.",
|
||||
},
|
||||
{
|
||||
n: "2",
|
||||
t: "Combine Like Terms",
|
||||
d: "Simplify each side separately.",
|
||||
},
|
||||
{
|
||||
n: "3",
|
||||
t: "Move",
|
||||
d: "Get variable terms on one side, constants on the other.",
|
||||
},
|
||||
{
|
||||
n: "4",
|
||||
t: "Divide",
|
||||
d: "Divide both sides by the coefficient of the variable.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.n}
|
||||
className="flex gap-3 bg-white/60 rounded-xl p-3 border border-blue-100"
|
||||
>
|
||||
<div className="w-7 h-7 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-sm shrink-0">
|
||||
{s.n}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 text-sm">{s.t}</p>
|
||||
<p className="text-slate-600 text-xs">{s.d}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: With Fractions" color="blue">
|
||||
<p>
|
||||
Solve: <Frac n="x" d="3" /> + 2 = <Frac n="x" d="2" /> − 1
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Multiply every term by 6 (LCD of 3 and 2):
|
||||
</p>
|
||||
<p className="text-slate-500">2x + 12 = 3x − 6</p>
|
||||
<p className="text-slate-500">
|
||||
12 + 6 = 3x − 2x → <strong className="text-blue-700">x = 18</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Literal Equation" color="blue">
|
||||
<p>Solve for r: A = P(1 + rt)</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by P: <Frac n="A" d="P" /> = 1 + rt
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Subtract 1: <Frac n="A" d="P" /> − 1 = rt
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by t:{" "}
|
||||
<strong className="text-blue-700">
|
||||
r = (<Frac n="A" d="P" /> − 1) ÷ t
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Practice: Rearranging Formulas
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Choose the correct operation at each step to isolate the target
|
||||
variable.
|
||||
</p>
|
||||
<LiteralEquationWidget />
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
When distributing a negative: −3(x − 4) = −3x{" "}
|
||||
<strong>+ 12</strong>, NOT −3x − 12.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Section 3: Variables on Both Sides ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Variables on Both Sides
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
When the variable appears on <strong>both sides</strong>, collect
|
||||
all variable terms on one side and all constants on the other. Then
|
||||
solve normally.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Variables Both Sides" color="blue">
|
||||
<p>Solve: 5x − 3 = 2x + 12</p>
|
||||
<p className="text-slate-500">Subtract 2x: 3x − 3 = 12</p>
|
||||
<p className="text-slate-500">Add 3: 3x = 15</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by 3: <strong className="text-blue-700">x = 5</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: With Distribution" color="blue">
|
||||
<p>Solve: 3(2x − 4) = 2(x + 6)</p>
|
||||
<p className="text-slate-500">Distribute: 6x − 12 = 2x + 12</p>
|
||||
<p className="text-slate-500">Subtract 2x: 4x − 12 = 12</p>
|
||||
<p className="text-slate-500">
|
||||
Add 12: 4x = 24 → <strong className="text-blue-700">x = 6</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Section 4: Number of Solutions ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Number of Solutions
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
After simplifying, three things can happen:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
<div className="bg-emerald-50/80 border border-emerald-200 rounded-xl p-4 text-center">
|
||||
<p className="font-extrabold text-emerald-700 text-lg">
|
||||
One Solution
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mt-1">x = number</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
e.g., 2x = 10 → x = 5
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50/80 border border-amber-200 rounded-xl p-4 text-center">
|
||||
<p className="font-extrabold text-amber-700 text-lg">
|
||||
No Solution
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mt-1">false statement</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
e.g., 0 = 5 (contradiction)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-blue-50/80 border border-blue-200 rounded-xl p-4 text-center">
|
||||
<p className="font-extrabold text-blue-700 text-lg">
|
||||
Infinite Solutions
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mt-1">true identity</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
e.g., 0 = 0 (always true)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: No Solution" color="blue">
|
||||
<p>Solve: 2(x + 3) = 2x + 10</p>
|
||||
<p className="text-slate-500">Distribute: 2x + 6 = 2x + 10</p>
|
||||
<p className="text-slate-500">
|
||||
Subtract 2x: 6 = 10 ←{" "}
|
||||
<strong className="text-rose-600">FALSE → No solution</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Infinite Solutions" color="blue">
|
||||
<p>Solve: 3(x + 2) = 3x + 6</p>
|
||||
<p className="text-slate-500">Distribute: 3x + 6 = 3x + 6</p>
|
||||
<p className="text-slate-500">
|
||||
Subtract 3x: 6 = 6 ←{" "}
|
||||
<strong className="text-blue-700">
|
||||
TRUE → Infinitely many solutions
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<LinearSolutionsWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
<strong>SAT Shortcut:</strong> For "what value of k gives
|
||||
infinitely many solutions?", make coefficients and constants match
|
||||
on both sides. For "no solution", make coefficients match but
|
||||
constants differ.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Section 5: Word Problems ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Word Problems
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The SAT tests your ability to{" "}
|
||||
<strong>translate words into equations</strong>. Look for key
|
||||
phrases:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3 mt-3 text-sm">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800">"is" / "equals"</p>
|
||||
<p className="text-slate-500">→ =</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800">
|
||||
"more than" / "increased by"
|
||||
</p>
|
||||
<p className="text-slate-500">→ +</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800">
|
||||
"less than" / "decreased by"
|
||||
</p>
|
||||
<p className="text-slate-500">→ −</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800">"of" / "times"</p>
|
||||
<p className="text-slate-500">→ ×</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Gym Membership" color="blue">
|
||||
<p>
|
||||
A gym charges $36 enrollment + $19/month. After how many months is
|
||||
the total $188?
|
||||
</p>
|
||||
<p className="text-slate-500">Equation: 36 + 19m = 188</p>
|
||||
<p className="text-slate-500">19m = 152</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">m = 8 months</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* ── Section 6: Practice & Quiz ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-4">
|
||||
Try These SAT Problems
|
||||
</h3>
|
||||
{LINEAR_EQ_ONE_VAR_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
{LINEAR_EQ_ONE_VAR_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
247
src/pages/student/lessons/LinearEq2VarLesson.tsx
Normal file
247
src/pages/student/lessons/LinearEq2VarLesson.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Grid,
|
||||
TrendingUp,
|
||||
Layers,
|
||||
ArrowRight,
|
||||
Hash,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import SlopeInterceptWidget from "../../../components/lessons/SlopeInterceptWidget";
|
||||
import ParallelPerpendicularWidget from "../../../components/lessons/ParallelPerpendicularWidget";
|
||||
import StandardFormWidget from "../../../components/lessons/StandardFormWidget";
|
||||
import {
|
||||
LINEAR_EQ_TWO_VAR_EASY,
|
||||
LINEAR_EQ_TWO_VAR_MEDIUM,
|
||||
} from "../../../data/math/linear-equations-two-var";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ title: "Slope", icon: TrendingUp },
|
||||
{ title: "Slope-Intercept Form", icon: Grid },
|
||||
{ title: "Point-Slope & Standard Form", icon: Hash },
|
||||
{ title: "Parallel & Perpendicular", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function LinearEq2VarLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Linear Equations in Two Variables"
|
||||
sections={SECTIONS}
|
||||
color="blue"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* ── Slope ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Understanding Slope
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Slope</strong> measures steepness — the rate of change
|
||||
between two points. It tells you how much y changes for each unit
|
||||
increase in x.
|
||||
</p>
|
||||
<FormulaBox>m = (y₂ − y₁) ÷ (x₂ − x₁) = rise ÷ run</FormulaBox>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-center">
|
||||
<p className="font-bold text-blue-700">Positive</p>
|
||||
<p className="text-xs text-slate-500">↗ rises left to right</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-center">
|
||||
<p className="font-bold text-rose-600">Negative</p>
|
||||
<p className="text-xs text-slate-500">↘ falls left to right</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-center">
|
||||
<p className="font-bold text-slate-600">Zero</p>
|
||||
<p className="text-xs text-slate-500">→ horizontal line</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-center">
|
||||
<p className="font-bold text-slate-600">Undefined</p>
|
||||
<p className="text-xs text-slate-500">↑ vertical line</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Find the Slope" color="blue">
|
||||
<p>Points: (2, 3) and (5, 9)</p>
|
||||
<p className="text-slate-500">
|
||||
m = (9 − 3) ÷ (5 − 2) = 6 ÷ 3 ={" "}
|
||||
<strong className="text-blue-700">2</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* ── Slope-Intercept ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Slope-Intercept Form
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The most common form for graphing and SAT questions:
|
||||
</p>
|
||||
<FormulaBox>y = mx + b</FormulaBox>
|
||||
<p className="text-slate-600 text-sm mt-2">
|
||||
<strong>m</strong> = slope (rate of change) | {" "}
|
||||
<strong>b</strong> = y-intercept (where line crosses y-axis)
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Write the Equation" color="blue">
|
||||
<p>A line has slope 3 and passes through (0, −2).</p>
|
||||
<p className="text-slate-500">
|
||||
Since it passes through (0, −2), the y-intercept b = −2.
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">y = 3x − 2</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<SlopeInterceptWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
On the SAT, "rate" or "per" usually indicates the slope. A "flat
|
||||
fee" or "starting value" is the y-intercept.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Point-Slope & Standard Form ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Point-Slope & Standard Form
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Point-slope form</strong> — useful when you know a point and
|
||||
the slope:
|
||||
</p>
|
||||
<FormulaBox>y − y₁ = m(x − x₁)</FormulaBox>
|
||||
<p className="text-slate-700 leading-relaxed mt-4">
|
||||
<strong>Standard form</strong> — Ax + By = C where A, B, C are
|
||||
integers:
|
||||
</p>
|
||||
<FormulaBox>Ax + By = C</FormulaBox>
|
||||
<p className="text-slate-600 text-sm mt-2">
|
||||
x-intercept: set y = 0 → x = C ÷ A | y-intercept: set x
|
||||
= 0 → y = C ÷ B
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Point-Slope" color="blue">
|
||||
<p>Slope = −2, passes through (4, 1).</p>
|
||||
<p className="text-slate-500">y − 1 = −2(x − 4)</p>
|
||||
<p className="text-slate-500">y − 1 = −2x + 8</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">y = −2x + 9</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard
|
||||
title="Example: Standard to Slope-Intercept"
|
||||
color="blue"
|
||||
>
|
||||
<p>Convert 3x + 4y = 12 to slope-intercept form.</p>
|
||||
<p className="text-slate-500">4y = −3x + 12</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">
|
||||
y = −<Frac n="3" d="4" />x + 3
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Explore Standard Form
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Adjust A, B, and C to see how the line and intercepts change in real
|
||||
time.
|
||||
</p>
|
||||
<StandardFormWidget />
|
||||
</div>
|
||||
|
||||
{/* ── Parallel & Perpendicular ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Parallel & Perpendicular Lines
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-blue-50/80 border border-blue-200 rounded-xl p-4">
|
||||
<p className="font-extrabold text-blue-800 mb-2">
|
||||
Parallel Lines
|
||||
</p>
|
||||
<FormulaBox>m₁ = m₂</FormulaBox>
|
||||
<p className="text-slate-600 text-sm mt-2">
|
||||
Same slope, different y-intercepts. They never intersect.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50/80 border border-violet-200 rounded-xl p-4">
|
||||
<p className="font-extrabold text-violet-800 mb-2">
|
||||
Perpendicular Lines
|
||||
</p>
|
||||
<FormulaBox>m₁ × m₂ = −1</FormulaBox>
|
||||
<p className="text-slate-600 text-sm mt-2">
|
||||
Slopes are <strong>negative reciprocals</strong>. They form 90°
|
||||
angles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Finding Perpendicular Slope" color="blue">
|
||||
<p>
|
||||
Line L: y = <Frac n="2" d="5" />x − 3. Find the slope of a
|
||||
perpendicular line.
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Slope of L: m = <Frac n="2" d="5" />
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Perpendicular slope = −<Frac n="5" d="2" /> (flip and negate)
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ParallelPerpendicularWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
Parallel = <strong>same slope</strong>. Perpendicular ={" "}
|
||||
<strong>negative reciprocal</strong> (flip the fraction and change
|
||||
the sign).
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Practice & Quiz ── */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-4">
|
||||
Try These SAT Problems
|
||||
</h3>
|
||||
{LINEAR_EQ_TWO_VAR_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
{LINEAR_EQ_TWO_VAR_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
504
src/pages/student/lessons/LinearEquationsLesson.tsx
Normal file
504
src/pages/student/lessons/LinearEquationsLesson.tsx
Normal file
@ -0,0 +1,504 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
Scale,
|
||||
ArrowRight,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import LinearSolutionsWidget from "../../../components/lessons/LinearSolutionsWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { LINEAR_EQ_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const BalanceScaleWidget = () => {
|
||||
const [left, setLeft] = useState(15);
|
||||
const [right, setRight] = useState(15);
|
||||
const [tilt, setTilt] = useState(0);
|
||||
const [message, setMessage] = useState("Balanced");
|
||||
|
||||
const apply = (val: number, side: "both" | "left" | "right") => {
|
||||
let newLeft = left;
|
||||
let newRight = right;
|
||||
if (side === "left" || side === "both") newLeft += val;
|
||||
if (side === "right" || side === "both") newRight += val;
|
||||
setLeft(newLeft);
|
||||
setRight(newRight);
|
||||
if (newLeft === newRight) {
|
||||
setTilt(0);
|
||||
setMessage("Perfectly Balanced! ✅");
|
||||
} else if (newLeft > newRight) {
|
||||
setTilt(-15);
|
||||
setMessage("Unbalanced! Left side is heavier. ❌");
|
||||
} else {
|
||||
setTilt(15);
|
||||
setMessage("Unbalanced! Right side is heavier. ❌");
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setLeft(15);
|
||||
setRight(15);
|
||||
setTilt(0);
|
||||
setMessage("Balanced");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h3 className="font-bold text-slate-700">Algebra Balance Scale</h3>
|
||||
<button onClick={reset} className="text-slate-400 hover:text-slate-600">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative h-40 w-full mb-8 flex justify-center items-end">
|
||||
<div className="absolute bottom-0 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-b-[40px] border-b-slate-800"></div>
|
||||
<div
|
||||
className="w-64 h-2 bg-slate-600 absolute bottom-[40px] transition-transform duration-700"
|
||||
style={{ transform: `rotate(${tilt}deg)` }}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 flex flex-col items-center"
|
||||
style={{ transform: `translateY(0px) rotate(${-tilt}deg)` }}
|
||||
>
|
||||
<div className="w-20 h-20 border-b-4 border-l-2 border-r-2 border-slate-400 rounded-b-xl bg-slate-50 flex items-center justify-center shadow-inner relative top-2">
|
||||
<span className="font-bold text-blue-600 text-lg">{left} kg</span>
|
||||
</div>
|
||||
<div className="w-0.5 h-16 bg-slate-400 absolute -top-16"></div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute right-0 top-0 flex flex-col items-center"
|
||||
style={{ transform: `translateY(0px) rotate(${-tilt}deg)` }}
|
||||
>
|
||||
<div className="w-20 h-20 border-b-4 border-l-2 border-r-2 border-slate-400 rounded-b-xl bg-slate-50 flex items-center justify-center shadow-inner relative top-2">
|
||||
<span className="font-bold text-emerald-600 text-lg">
|
||||
{right} kg
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-0.5 h-16 bg-slate-400 absolute -top-16"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`text-center font-bold mb-6 ${tilt === 0 ? "text-green-600" : "text-rose-600"}`}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => apply(-5, "left")}
|
||||
className="p-3 rounded-lg border-2 border-slate-200 hover:border-rose-300 hover:bg-rose-50 text-slate-600 text-sm font-bold transition-all"
|
||||
>
|
||||
Subtract 5 from Left ONLY
|
||||
</button>
|
||||
<button
|
||||
onClick={() => apply(-5, "both")}
|
||||
className="p-3 rounded-lg border-2 border-blue-500 bg-blue-50 hover:bg-blue-100 text-blue-700 text-sm font-bold transition-all shadow-sm"
|
||||
>
|
||||
Subtract 5 from BOTH sides
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-center text-slate-400 mt-4">
|
||||
Goal: Keep the scale balanced while isolating the variable.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinearEquationsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-blue-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-blue-600 text-white" : isPast ? "bg-blue-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-blue-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="The Balance Principle" icon={Scale} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Number of Solutions"
|
||||
icon={ArrowRight}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Balance Principle */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
The Balance Principle
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-6">
|
||||
<p>
|
||||
Linear equations are like a balance scale. Whatever you do to one
|
||||
side, you <strong>must</strong> do to the other. The goal is
|
||||
always to <strong>isolate the variable</strong> — get x by itself
|
||||
on one side with a number on the other.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BalanceScaleWidget />
|
||||
|
||||
{/* 4-Step Process */}
|
||||
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-2xl p-6 space-y-4">
|
||||
<h3 className="font-bold text-blue-900 text-lg">
|
||||
The 4-Step Solve Process
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
step: "1",
|
||||
title: "Distribute",
|
||||
desc: "Expand parentheses and clear fractions by multiplying through by the LCD (lowest common denominator).",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
title: "Combine Like Terms",
|
||||
desc: "Simplify each side of the equation separately — add/subtract terms with the same variable.",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
title: "Move",
|
||||
desc: "Get all variable terms on one side, all constant terms on the other, using addition or subtraction.",
|
||||
},
|
||||
{
|
||||
step: "4",
|
||||
title: "Divide",
|
||||
desc: "Divide both sides by the coefficient of the variable to solve.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.step}
|
||||
className="flex gap-4 bg-white rounded-xl p-4 border border-blue-100"
|
||||
>
|
||||
<div className="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold shrink-0">
|
||||
{item.step}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">{item.title}</p>
|
||||
<p className="text-slate-600 text-sm">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Three Worked Examples */}
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Example 1: Basic (no fractions)
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Solve: 3(2x − 4) = 2x + 8</p>
|
||||
<p className="text-slate-500">
|
||||
Step 1 (Distribute): 6x − 12 = 2x + 8
|
||||
</p>
|
||||
<p className="text-slate-500">Step 3 (Move): 4x = 20</p>
|
||||
<p className="text-slate-500">
|
||||
Step 4 (Divide by 4):{" "}
|
||||
<strong className="text-emerald-700">x = 5</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Example 2: With fractions — clear them first!
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>
|
||||
Solve: <Frac n="x" d="3" /> + 2 = <Frac n="x" d="2" /> − 1
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Multiply every term by 6 (LCD of 3 and 2):
|
||||
</p>
|
||||
<p className="text-slate-500">2x + 12 = 3x − 6</p>
|
||||
<p className="text-slate-500">12 + 6 = 3x − 2x</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">x = 18</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Example 3: Literal equations (isolating a variable)
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Solve for r: A = P(1 + rt)</p>
|
||||
<p className="text-slate-500">
|
||||
Divide both sides by P: <Frac n="A" d="P" /> = 1 + rt
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Subtract 1: <Frac n="A" d="P" /> − 1 = rt
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by t:{" "}
|
||||
<strong className="text-emerald-700">
|
||||
r ={" "}
|
||||
<Frac
|
||||
n={
|
||||
<>
|
||||
<Frac n="A" d="P" /> − 1
|
||||
</>
|
||||
}
|
||||
d="t"
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
Common SAT Mistake: Distributing a Negative
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
When distributing a negative sign: −3(x − 4) = −3x + 12, NOT −3x −
|
||||
12. The negative multiplies EVERY term inside the parentheses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-8 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Number of Solutions{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Number of Solutions */}
|
||||
<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-6">
|
||||
One, None, or Infinite Solutions?
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Not all equations have exactly one answer. After simplifying, look
|
||||
at what's left. The SAT frequently asks for a value of <em>k</em>{" "}
|
||||
that makes an equation have no solution or infinitely many
|
||||
solutions — this is a critical concept to master.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
The Three Outcomes
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-blue-100 rounded-xl p-4 text-center">
|
||||
<div className="font-bold text-blue-900 text-base mb-2">
|
||||
One Solution
|
||||
</div>
|
||||
<div className="font-mono text-sm text-blue-700 bg-white rounded p-2 mb-2">
|
||||
2x + 1 = x + 5
|
||||
</div>
|
||||
<div className="text-xs text-blue-700">
|
||||
Variable survives after simplification → unique answer:{" "}
|
||||
<strong>x = 4</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-center">
|
||||
<div className="font-bold text-red-900 text-base mb-2">
|
||||
No Solution
|
||||
</div>
|
||||
<div className="font-mono text-sm text-red-700 bg-white rounded p-2 mb-2">
|
||||
2x + 3 = 2x + 5
|
||||
</div>
|
||||
<div className="text-xs text-red-700">
|
||||
Variables cancel → <strong>3 = 5</strong> (false statement —
|
||||
impossible)
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 text-center">
|
||||
<div className="font-bold text-emerald-900 text-base mb-2">
|
||||
Infinite Solutions
|
||||
</div>
|
||||
<div className="font-mono text-sm text-emerald-700 bg-white rounded p-2 mb-2">
|
||||
2x + 3 = 2x + 3
|
||||
</div>
|
||||
<div className="text-xs text-emerald-700">
|
||||
Variables cancel → <strong>3 = 3</strong> (always true — every
|
||||
x works)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Finding k */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
SAT Focus: Finding the Value of k
|
||||
</h3>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
Example: For what value of k does 4x + k = 4x − 2 have no
|
||||
solution?
|
||||
</p>
|
||||
<div className="font-mono text-sm text-slate-700 space-y-1">
|
||||
<p>After subtracting 4x from both sides: k = −2</p>
|
||||
<p>
|
||||
If k = −2: −2 = −2 (always true) → infinite solutions, not no
|
||||
solution!
|
||||
</p>
|
||||
<p className="text-blue-700 font-bold">
|
||||
For no solution: k ≠ −2 (any other value gives a false
|
||||
statement like k = −2 being false)
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs mt-2">
|
||||
The equation has no solution for any value of k EXCEPT −2. For
|
||||
infinite solutions, set k = −2.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
Example: For what value of k does 3(x + k) = 3x + 12 have
|
||||
infinite solutions?
|
||||
</p>
|
||||
<div className="font-mono text-sm text-slate-700 space-y-1">
|
||||
<p>Distribute: 3x + 3k = 3x + 12</p>
|
||||
<p>Subtract 3x: 3k = 12</p>
|
||||
<p className="text-blue-700 font-bold">k = 4</p>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs mt-2">
|
||||
For k = 4: 12 = 12 (always true) → infinite solutions. ✓
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">The Key Rule</p>
|
||||
<p className="text-slate-700">
|
||||
<strong>No solution</strong>: same variable coefficients,
|
||||
different constants (parallel lines).
|
||||
<br />
|
||||
<strong>Infinite solutions</strong>: same variable coefficients
|
||||
AND same constants (identical lines).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinearSolutionsWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[2] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{LINEAR_EQ_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-blue-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-blue-900 font-bold rounded-full hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinearEquationsLesson;
|
||||
226
src/pages/student/lessons/LinearFunctionsLesson.tsx
Normal file
226
src/pages/student/lessons/LinearFunctionsLesson.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import React from "react";
|
||||
import {
|
||||
TrendingUp,
|
||||
Hash,
|
||||
ArrowRight,
|
||||
Lightbulb,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import LinearTransformationWidget from "../../../components/lessons/LinearTransformationWidget";
|
||||
import {
|
||||
LINEAR_FUNC_EASY,
|
||||
LINEAR_FUNC_MEDIUM,
|
||||
} from "../../../data/math/linear-functions";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ title: "Function Notation", icon: Hash },
|
||||
{ title: "Evaluating Functions", icon: ArrowRight },
|
||||
{ title: "Interpreting Slope & Intercepts", icon: TrendingUp },
|
||||
{ title: "Domain & Range", icon: Lightbulb },
|
||||
{ title: "Function Transformations", icon: TrendingUp },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function LinearFunctionsLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Linear Functions"
|
||||
sections={SECTIONS}
|
||||
color="blue"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Function Notation
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>f(x)</strong> is read "f of x" — it names the output when
|
||||
the input is x. It does NOT mean f × x.
|
||||
</p>
|
||||
<FormulaBox>f(x) = mx + b</FormulaBox>
|
||||
<p className="text-slate-600 text-sm mt-2">
|
||||
f(x) is just another way of writing y. Any letter can name a
|
||||
function: g(x), h(t), etc.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Evaluate f(4)" color="blue">
|
||||
<p>f(x) = 2x + 3</p>
|
||||
<p className="text-slate-500">
|
||||
f(4) = 2(4) + 3 = 8 + 3 ={" "}
|
||||
<strong className="text-blue-700">11</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
f(a) = 0 means "find the x where the output is zero" — this is the
|
||||
x-intercept.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Evaluating Functions
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
To evaluate f(a), replace every x with a and simplify. You can also
|
||||
evaluate from graphs or tables.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Composition" color="blue">
|
||||
<p>f(x) = 2x + 1, g(x) = x². Find f(g(3)).</p>
|
||||
<p className="text-slate-500">
|
||||
g(3) = 9, then f(9) = 2(9) + 1 ={" "}
|
||||
<strong className="text-blue-700">19</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: From a Table" color="blue">
|
||||
<p>If f(2) = 7 and f(5) = 13, find the slope.</p>
|
||||
<p className="text-slate-500">
|
||||
m = (13 − 7) ÷ (5 − 2) = 6 ÷ 3 ={" "}
|
||||
<strong className="text-blue-700">2</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Interpreting Slope & Intercepts
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
In context, <strong>slope</strong> = rate of change per unit,{" "}
|
||||
<strong>y-intercept</strong> = starting/initial value.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
slope = Δy ÷ Δx = change in output ÷ change in input
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Cost Function" color="blue">
|
||||
<p>C(m) = 30m + 50 models gym cost after m months.</p>
|
||||
<p className="text-slate-500">
|
||||
Slope 30 → cost increases <strong>$30/month</strong>
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Intercept 50 → <strong>$50 signup fee</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
SAT: "What does 30 represent?" = interpret slope. "What does 50
|
||||
represent?" = interpret y-intercept.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Domain & Range
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Domain</strong> = all possible x values.{" "}
|
||||
<strong>Range</strong> = all possible y values.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 text-sm">
|
||||
Linear Functions
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Domain & range: all real numbers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 text-sm">In Context</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Domain may be restricted (e.g., m ≥ 0).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Restricted Domain" color="blue">
|
||||
<p>T(d) = 80 − 3d, where 0 ≤ d ≤ 20.</p>
|
||||
<p className="text-slate-500">
|
||||
Range: T(20) = 20 to T(0) = 80, so{" "}
|
||||
<strong className="text-blue-700">20 ≤ T ≤ 80</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Function Transformations
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<div className="space-y-2 text-sm">
|
||||
{[
|
||||
["f(x) + k", "Up k"],
|
||||
["f(x) − k", "Down k"],
|
||||
["f(x − h)", "Right h"],
|
||||
["f(x + h)", "Left h"],
|
||||
["−f(x)", "Reflect x-axis"],
|
||||
["af(x), |a|>1", "Stretch"],
|
||||
["af(x), |a|<1", "Compress"],
|
||||
].map(([c, d]) => (
|
||||
<div
|
||||
key={c}
|
||||
className="flex gap-3 items-center bg-white/60 rounded-lg p-3 border border-blue-100"
|
||||
>
|
||||
<code className="font-mono text-blue-700 font-bold min-w-[110px]">
|
||||
{c}
|
||||
</code>
|
||||
<span className="text-slate-600">{d}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<LinearTransformationWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
Horizontal shifts are <strong>opposite</strong>: f(x − 3) shifts{" "}
|
||||
<strong>right</strong> 3, not left!
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-4">
|
||||
Try These SAT Problems
|
||||
</h3>
|
||||
{LINEAR_FUNC_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
{LINEAR_FUNC_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
303
src/pages/student/lessons/LinearInequalitiesLesson.tsx
Normal file
303
src/pages/student/lessons/LinearInequalitiesLesson.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Scale,
|
||||
ArrowRight,
|
||||
GitBranch,
|
||||
BarChart,
|
||||
Layers,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import InequalityRegionWidget from "../../../components/lessons/InequalityRegionWidget";
|
||||
import {
|
||||
LINEAR_INEQ_EASY,
|
||||
LINEAR_INEQ_MEDIUM,
|
||||
} from "../../../data/math/linear-inequalities";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Solving Inequalities", icon: Scale },
|
||||
{ title: "Compound Inequalities", icon: GitBranch },
|
||||
{ title: "Graphing on Number Lines", icon: ArrowRight },
|
||||
{ title: "Coordinate Plane", icon: BarChart },
|
||||
{ title: "Systems of Inequalities", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function LinearInequalitiesLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Linear Inequalities"
|
||||
sections={SECTIONS}
|
||||
color="blue"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Solving Inequalities */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Solving Inequalities
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Inequalities work just like equations — apply the same operation to
|
||||
both sides. The <strong>one critical difference</strong>: when you
|
||||
multiply or divide by a <strong>negative number</strong>, you must{" "}
|
||||
<strong>flip the inequality sign</strong>.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700 text-lg"><</p>
|
||||
<p className="text-xs text-slate-500">Less than</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700 text-lg">></p>
|
||||
<p className="text-xs text-slate-500">Greater than</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700 text-lg">≤</p>
|
||||
<p className="text-xs text-slate-500">Less than or equal</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700 text-lg">≥</p>
|
||||
<p className="text-xs text-slate-500">Greater than or equal</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Flip the Sign" color="blue">
|
||||
<p>Solve: −3x + 7 > 16</p>
|
||||
<p className="text-slate-500">−3x > 9</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by −3 → <strong className="text-red-600">FLIP</strong> → x
|
||||
< −3
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Variables on Both Sides" color="blue">
|
||||
<p>Solve: 2x − 5 ≤ 3x + 2</p>
|
||||
<p className="text-slate-500">−x ≤ 7</p>
|
||||
<p className="text-slate-500">
|
||||
Multiply by −1 → <strong className="text-blue-700">x ≥ −7</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
The #1 inequality mistake: forgetting to flip the sign when
|
||||
multiplying or dividing by a negative. The SAT specifically
|
||||
designs trap answers for this.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Compound Inequalities */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Compound Inequalities
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A compound inequality combines two inequalities.{" "}
|
||||
<strong>AND</strong> means both must be true (intersection).{" "}
|
||||
<strong>OR</strong> means at least one must be true (union).
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-800 mb-1">
|
||||
AND (Intersection)
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
−2 < x ≤ 5 means x is between −2 and 5
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Both conditions satisfied simultaneously
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 mb-1">OR (Union)</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
x < −3 OR x > 4 means outside the interval
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
At least one condition satisfied
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: AND (Three-Part)" color="blue">
|
||||
<p>Solve: −2 < 3x + 1 ≤ 10</p>
|
||||
<p className="text-slate-500">Subtract 1: −3 < 3x ≤ 9</p>
|
||||
<p className="text-slate-500">
|
||||
Divide by 3:{" "}
|
||||
<strong className="text-blue-700">−1 < x ≤ 3</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: OR" color="blue">
|
||||
<p>Solve: x + 1 < −2 OR x + 1 > 4</p>
|
||||
<p className="text-slate-500">x < −3 OR x > 3</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">
|
||||
Solution: (−∞, −3) ∪ (3, ∞)
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Graphing on Number Lines */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Graphing on Number Lines
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
When graphing inequalities on a number line, the circle type and
|
||||
shading direction matter.
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-blue-900">
|
||||
<th className="border border-blue-300 px-3 py-2 text-left font-bold">
|
||||
Symbol
|
||||
</th>
|
||||
<th className="border border-blue-300 px-3 py-2 text-left font-bold">
|
||||
Circle
|
||||
</th>
|
||||
<th className="border border-blue-300 px-3 py-2 text-left font-bold">
|
||||
Meaning
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
< or >
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Open ○
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Value NOT included
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
≤ or ≥
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Closed ●
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Value IS included
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
The SAT frequently asks which number line graph matches a given
|
||||
inequality. Just check: open vs closed circle, and which direction
|
||||
the arrow points.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Coordinate Plane */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Coordinate Plane Inequalities
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
When graphing a linear inequality in two variables:
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm">
|
||||
<p>1. Graph the boundary line (y = mx + b)</p>
|
||||
<p>
|
||||
2. Use a <strong>dashed line</strong> for < or > (not
|
||||
included)
|
||||
</p>
|
||||
<p>
|
||||
3. Use a <strong>solid line</strong> for ≤ or ≥ (included)
|
||||
</p>
|
||||
<p>
|
||||
4. Shade <strong>above</strong> for y > or y ≥ and{" "}
|
||||
<strong>below</strong> for y < or y ≤
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Graph y ≥ 2x − 1" color="blue">
|
||||
<p>Boundary line: y = 2x − 1 (slope 2, y-intercept −1)</p>
|
||||
<p className="text-slate-500">Solid line (≥ means included)</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">Shade above the line</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<InequalityRegionWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Systems of Inequalities */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Systems of Inequalities
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The solution to a system of inequalities is the{" "}
|
||||
<strong>overlapping region</strong> where ALL inequalities are
|
||||
satisfied. Any point in this region satisfies every inequality in
|
||||
the system.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: System" color="blue">
|
||||
<p>y ≤ x + 3 AND y > −x + 1</p>
|
||||
<p className="text-slate-500">
|
||||
Graph both: shade below y = x + 3 (solid), shade above y = −x + 1
|
||||
(dashed)
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">
|
||||
Solution is the overlapping region
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
To check if a point is in the solution region, plug it into BOTH
|
||||
inequalities. It must satisfy all of them.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{LINEAR_INEQ_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
{LINEAR_INEQ_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
350
src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx
Normal file
350
src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx
Normal file
@ -0,0 +1,350 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Layers } from "lucide-react";
|
||||
import ParallelPerpendicularWidget from "../../../components/lessons/ParallelPerpendicularWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { LINEAR_PARALLEL_PERP_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const LinearParallelPerpendicularLesson: React.FC<LessonProps> = ({
|
||||
onFinish,
|
||||
}) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-blue-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-blue-600 text-white" : isPast ? "bg-blue-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-blue-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker
|
||||
index={0}
|
||||
title="Parallel & Perpendicular"
|
||||
icon={Layers}
|
||||
/>
|
||||
<SectionMarker index={1} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1 */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Parallel & Perpendicular Lines
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Parallel and perpendicular line questions appear on almost every
|
||||
SAT. The core skill is: identify the slope of the given line,
|
||||
apply the parallel or perpendicular slope rule, then write the new
|
||||
equation through a given point.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
The Two Slope Rules
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-900 mb-3 text-lg">
|
||||
Parallel Lines
|
||||
</p>
|
||||
<div className="bg-blue-50 rounded-lg p-3 text-center mb-3">
|
||||
<p className="font-mono text-blue-800 font-bold text-xl">
|
||||
m₁ = m₂
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Same slope, different y-intercept
|
||||
</p>
|
||||
</div>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>Lines never intersect — they run side by side</li>
|
||||
<li>Same slope guarantees they won't cross</li>
|
||||
<li>
|
||||
If y-intercepts also matched, the lines would be identical
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-3 bg-blue-50 rounded-lg p-2 font-mono text-xs text-slate-600">
|
||||
y = 3x + 1 ∥ y = 3x − 7 (both slope = 3)
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-indigo-900 mb-3 text-lg">
|
||||
Perpendicular Lines
|
||||
</p>
|
||||
<div className="bg-indigo-50 rounded-lg p-3 text-center mb-3">
|
||||
<p className="font-mono text-indigo-800 font-bold text-xl">
|
||||
m₁ × m₂ = −1
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Negative reciprocal slopes
|
||||
</p>
|
||||
</div>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>Lines meet at a 90° angle</li>
|
||||
<li>Rule: flip the fraction and change the sign</li>
|
||||
<li>
|
||||
A horizontal line (slope 0) is ⊥ to a vertical line
|
||||
(undefined slope)
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-3 bg-indigo-50 rounded-lg p-2 font-mono text-xs text-slate-600">
|
||||
y = <Frac n="2" d="3" />x + 1 ⊥ y = <Frac n="−3" d="2" />x + 5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Negative Reciprocal Examples */}
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
Finding Perpendicular Slopes: Worked Examples
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-blue-900">
|
||||
<th className="p-2 text-left font-bold">
|
||||
Original Slope
|
||||
</th>
|
||||
<th className="p-2 text-left font-bold">
|
||||
Step 1: Flip the fraction
|
||||
</th>
|
||||
<th className="p-2 text-left font-bold">
|
||||
Step 2: Negate the sign
|
||||
</th>
|
||||
<th className="p-2 text-left font-bold">
|
||||
Perpendicular Slope
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-600">
|
||||
<tr className="border-b border-blue-50">
|
||||
<td className="p-2 font-mono">2 (= 2÷1)</td>
|
||||
<td className="p-2">
|
||||
<Frac n="1" d="2" />
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Frac n="−1" d="2" />
|
||||
</td>
|
||||
<td className="p-2 font-bold text-indigo-700">
|
||||
<Frac n="−1" d="2" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-blue-50 bg-slate-50">
|
||||
<td className="p-2">
|
||||
<Frac n="−3" d="4" />
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Frac n="4" d="3" />
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Frac n="4" d="3" />
|
||||
</td>
|
||||
<td className="p-2 font-bold text-indigo-700">
|
||||
<Frac n="4" d="3" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-blue-50">
|
||||
<td className="p-2 font-mono">−5</td>
|
||||
<td className="p-2">
|
||||
<Frac n="1" d="5" /> (flip)
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Frac n="1" d="5" /> (negate negative)
|
||||
</td>
|
||||
<td className="p-2 font-bold text-indigo-700">
|
||||
<Frac n="1" d="5" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="p-2 font-mono">0 (horizontal)</td>
|
||||
<td className="p-2 font-mono">—</td>
|
||||
<td className="p-2 font-mono">—</td>
|
||||
<td className="p-2 font-bold text-indigo-700">
|
||||
Undefined (vertical)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full Problem Worked Examples */}
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
Complete Problem: Writing the Equation
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-blue-800 mb-2">
|
||||
Example 1: Find the line parallel to y = 4x − 3 passing
|
||||
through (2, 5)
|
||||
</p>
|
||||
<div className="font-mono space-y-1 text-slate-700">
|
||||
<p>Parallel → same slope: m = 4</p>
|
||||
<p>Use point-slope: y − 5 = 4(x − 2)</p>
|
||||
<p>y − 5 = 4x − 8</p>
|
||||
<p className="text-blue-700 font-bold">
|
||||
y = 4x − 3 ← Wait, same as original! Confirm: passes
|
||||
through (2, 5): 5 = 8 − 3 = 5 ✓
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-indigo-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-indigo-800 mb-2">
|
||||
Example 2: Find the line perpendicular to 2x + 3y = 12
|
||||
passing through (4, 1)
|
||||
</p>
|
||||
<div className="font-mono space-y-1 text-slate-700">
|
||||
<p>
|
||||
First, find slope of 2x + 3y = 12: y ={" "}
|
||||
<Frac n="−2" d="3" />x + 4, so m = <Frac n="−2" d="3" />
|
||||
</p>
|
||||
<p>
|
||||
Perpendicular slope: flip and negate → m ={" "}
|
||||
<Frac n="3" d="2" />
|
||||
</p>
|
||||
<p>
|
||||
Point-slope: y − 1 = <Frac n="3" d="2" />
|
||||
(x − 4)
|
||||
</p>
|
||||
<p>
|
||||
y = <Frac n="3" d="2" />x − 6 + 1
|
||||
</p>
|
||||
<p className="text-indigo-700 font-bold">
|
||||
y = <Frac n="3" d="2" />x − 5
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
SAT Trap: Parallel Lines Must Have Different Intercepts
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Parallel lines need the <em>same slope</em> but a{" "}
|
||||
<em>different y-intercept</em>. If the intercepts also match,
|
||||
the lines are identical — infinitely many intersections, not
|
||||
parallel. The SAT sometimes includes a "same slope, same
|
||||
intercept" option to trap students.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParallelPerpendicularWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<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[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{LINEAR_PARALLEL_PERP_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-blue-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-blue-900 font-bold rounded-full hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinearParallelPerpendicularLesson;
|
||||
326
src/pages/student/lessons/LinearTransformationsLesson.tsx
Normal file
326
src/pages/student/lessons/LinearTransformationsLesson.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, TrendingUp } from "lucide-react";
|
||||
import LinearTransformationWidget from "../../../components/lessons/LinearTransformationWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { LINEAR_TRANSFORMATIONS_QUIZ_DATA } from "../../../utils/constants";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const LinearTransformationsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-blue-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-blue-600 text-white" : isPast ? "bg-blue-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-blue-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker
|
||||
index={0}
|
||||
title="Shift, Reflect & Scale"
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
<SectionMarker index={1} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1 */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Function Transformations
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Given a base function f(x), transformations let you shift, flip,
|
||||
and scale it predictably. These rules apply to <em>any</em>{" "}
|
||||
function type — linear, quadratic, absolute value, or otherwise.
|
||||
The SAT tests these with graphs, tables, and algebraic forms, so
|
||||
you must recognize them quickly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Transformation Table */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
All Six Transformations
|
||||
</h3>
|
||||
<div className="overflow-x-auto rounded-xl border border-blue-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-blue-900 text-white">
|
||||
<th className="p-3 text-left">Transformation</th>
|
||||
<th className="p-3 text-left">Notation</th>
|
||||
<th className="p-3 text-left">Effect on Graph</th>
|
||||
<th className="p-3 text-left">Effect on Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-blue-100">
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">Shift Up k units</td>
|
||||
<td className="p-3 font-mono text-blue-700">f(x) + k</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Entire graph moves up by k
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">(x, y) → (x, y + k)</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="p-3 font-bold">Shift Down k units</td>
|
||||
<td className="p-3 font-mono text-blue-700">f(x) − k</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Entire graph moves down by k
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">(x, y) → (x, y − k)</td>
|
||||
</tr>
|
||||
<tr className="bg-red-50">
|
||||
<td className="p-3 font-bold text-red-900">
|
||||
Shift Right h units
|
||||
</td>
|
||||
<td className="p-3 font-mono text-red-700">f(x − h)</td>
|
||||
<td className="p-3 text-red-800">Graph moves RIGHT by h</td>
|
||||
<td className="p-3 text-red-700">(x, y) → (x + h, y)</td>
|
||||
</tr>
|
||||
<tr className="bg-red-50">
|
||||
<td className="p-3 font-bold text-red-900">
|
||||
Shift Left h units
|
||||
</td>
|
||||
<td className="p-3 font-mono text-red-700">f(x + h)</td>
|
||||
<td className="p-3 text-red-800">Graph moves LEFT by h</td>
|
||||
<td className="p-3 text-red-700">(x, y) → (x − h, y)</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold">Reflect over x-axis</td>
|
||||
<td className="p-3 font-mono text-blue-700">−f(x)</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Graph flips vertically
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">(x, y) → (x, −y)</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="p-3 font-bold">Reflect over y-axis</td>
|
||||
<td className="p-3 font-mono text-blue-700">f(−x)</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Graph flips horizontally
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">(x, y) → (−x, y)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* The #1 Trap */}
|
||||
<div className="bg-red-100 border border-red-300 rounded-xl p-5">
|
||||
<p className="font-bold text-red-900 text-base mb-2">
|
||||
⚠ The #1 Trap — Horizontal Shifts Are BACKWARDS
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm mb-2">
|
||||
f(x − 3) shifts the graph <strong>right 3</strong> (NOT left).
|
||||
f(x + 2) shifts the graph <strong>left 2</strong> (NOT right).
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm">
|
||||
Why? Because to get the same y-value, x must be 3 larger. The
|
||||
shift in the graph is always <em>opposite</em> to the sign
|
||||
inside.
|
||||
</p>
|
||||
<div className="font-mono text-sm mt-2 bg-white rounded p-2 text-slate-700">
|
||||
<p>f(x) = x² has vertex at (0, 0)</p>
|
||||
<p>g(x) = (x − 3)² → vertex at (3, 0) → shifted RIGHT 3 ✓</p>
|
||||
<p>h(x) = (x + 2)² → vertex at (−2, 0) → shifted LEFT 2 ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Combined Transformations */}
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
Combined Transformations — Apply in Order
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
When multiple transformations are applied, you can read them
|
||||
directly from the equation. Apply in this order: horizontal
|
||||
shift → vertical stretch/compress → reflection → vertical shift.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-blue-800 mb-2">
|
||||
Example: g(x) = −f(x − 2) + 3
|
||||
</p>
|
||||
<div className="space-y-1 text-slate-700">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-red-600">
|
||||
→ shift right 2:
|
||||
</span>
|
||||
<span>f(x − 2) moves graph right 2</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-purple-600">
|
||||
→ reflect over x-axis:
|
||||
</span>
|
||||
<span>−f(...) flips graph vertically</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-blue-600">
|
||||
→ shift up 3:
|
||||
</span>
|
||||
<span>+ 3 moves graph up 3</span>
|
||||
</div>
|
||||
<p className="font-mono text-blue-700 font-bold mt-1">
|
||||
If f has point (4, 1), then g has point (4 + 2, −1 + 3) =
|
||||
(6, 2)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-blue-800 mb-2">
|
||||
Example: h(x) = 2f(x + 1) − 4
|
||||
</p>
|
||||
<div className="space-y-1 text-slate-700">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-red-600">
|
||||
→ shift left 1:
|
||||
</span>
|
||||
<span>f(x + 1) moves graph left 1</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-green-600">
|
||||
→ stretch vertically by 2:
|
||||
</span>
|
||||
<span>all y-values multiply by 2</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="font-bold text-blue-600">
|
||||
→ shift down 4:
|
||||
</span>
|
||||
<span>− 4 moves graph down 4</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SAT Question Type */}
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-sky-900 mb-1">
|
||||
SAT Question Type: Table of Values
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
The SAT may give you a table of values for f(x) and ask for
|
||||
values of g(x) = f(x − 2) + 1. Strategy: for each value in the
|
||||
g(x) table, work backwards. To find g(3), you need f(3 − 2) + 1
|
||||
= f(1) + 1. Look up f(1) in the original table, then add 1.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinearTransformationWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<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[1] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{LINEAR_TRANSFORMATIONS_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-blue-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-blue-900 font-bold rounded-full hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinearTransformationsLesson;
|
||||
715
src/pages/student/lessons/LinesAnglesLesson.tsx
Normal file
715
src/pages/student/lessons/LinesAnglesLesson.tsx
Normal file
@ -0,0 +1,715 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
Target,
|
||||
Layers,
|
||||
Calculator,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import InteractiveTransversal from "../../../components/lessons/InteractiveTransversal";
|
||||
import InteractiveTriangle from "../../../components/lessons/InteractiveTriangle";
|
||||
import PolygonWidget from "../../../components/lessons/PolygonWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { ANGLES_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const LinesAnglesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-emerald-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-emerald-600 text-white" : isPast ? "bg-emerald-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-emerald-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Parallel Lines" icon={Target} />
|
||||
<SectionMarker index={1} title="Triangles" icon={Layers} />
|
||||
<SectionMarker
|
||||
index={2}
|
||||
title="Special Triangles"
|
||||
icon={Calculator}
|
||||
/>
|
||||
<SectionMarker index={3} title="Polygons" icon={Calculator} />
|
||||
<SectionMarker index={4} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Parallel Lines */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Parallel Lines & Transversals
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
When two parallel lines are cut by a transversal, 8 angles are
|
||||
formed. They fall into exactly two groups:{" "}
|
||||
<strong>equal angles</strong> and{" "}
|
||||
<strong>supplementary pairs</strong> (summing to 180°). Know one
|
||||
angle → find all eight.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-emerald-900">
|
||||
The 5 Angle-Pair Relationships
|
||||
</h3>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-emerald-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-emerald-900 text-white">
|
||||
<th className="p-3 text-left">Angle Pair</th>
|
||||
<th className="p-3 text-left">Location</th>
|
||||
<th className="p-3 text-left">Relationship</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-emerald-100">
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-3 font-bold text-emerald-800">
|
||||
Corresponding
|
||||
</td>
|
||||
<td className="p-3">
|
||||
Same side, same position at each intersection
|
||||
</td>
|
||||
<td className="p-3 font-bold text-emerald-700">Equal</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold text-emerald-800">
|
||||
Alternate Interior
|
||||
</td>
|
||||
<td className="p-3">
|
||||
Between the parallel lines, opposite sides
|
||||
</td>
|
||||
<td className="p-3 font-bold text-emerald-700">Equal</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-3 font-bold text-emerald-800">
|
||||
Alternate Exterior
|
||||
</td>
|
||||
<td className="p-3">
|
||||
Outside the parallel lines, opposite sides
|
||||
</td>
|
||||
<td className="p-3 font-bold text-emerald-700">Equal</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold text-slate-600">
|
||||
Co-Interior (Same-Side)
|
||||
</td>
|
||||
<td className="p-3">
|
||||
Between the lines, same side of transversal
|
||||
</td>
|
||||
<td className="p-3 font-bold text-rose-700">
|
||||
Supplementary (sum = 180°)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-3 font-bold text-slate-600">
|
||||
Vertical Angles
|
||||
</td>
|
||||
<td className="p-3">
|
||||
Opposite each other at an intersection
|
||||
</td>
|
||||
<td className="p-3 font-bold text-emerald-700">Equal</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Worked Example */}
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-100">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Worked Example: Find All 8 Angles
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
If one angle formed by a transversal cutting two parallel lines
|
||||
is 65°, find all other angles.
|
||||
</p>
|
||||
<div className="bg-emerald-50 rounded-lg p-4 text-sm font-mono text-slate-700 space-y-1">
|
||||
<p>
|
||||
Angle 1 = <strong>65°</strong> (given)
|
||||
</p>
|
||||
<p>
|
||||
Vertical angle = <strong>65°</strong> (vertical angles are
|
||||
equal)
|
||||
</p>
|
||||
<p>
|
||||
Corresponding angle = <strong>65°</strong> (corresponding
|
||||
angles are equal)
|
||||
</p>
|
||||
<p>
|
||||
Its vertical angle = <strong>65°</strong>
|
||||
</p>
|
||||
<p>
|
||||
All four supplementary angles = 180° − 65° ={" "}
|
||||
<strong>115°</strong>
|
||||
</p>
|
||||
<p className="mt-2 text-emerald-800 font-bold">
|
||||
Result: four 65° angles and four 115° angles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-sky-900 mb-1">
|
||||
SAT Strategy: Label with x and 180 − x
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
When angles are expressed algebraically, label all equal angles
|
||||
as "x" and all supplementary angles as "180 − x." Then set equal
|
||||
or add to 180 to solve.
|
||||
</p>
|
||||
<div className="font-mono text-xs bg-white rounded p-2 mt-2 text-slate-700">
|
||||
<p>
|
||||
Example: Corresponding angles → 3x + 15 = 2x + 45 → x = 30°
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<InteractiveTransversal />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="group flex items-center text-emerald-600 font-bold hover:text-emerald-800"
|
||||
>
|
||||
Next: Triangles{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Triangles */}
|
||||
<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-6">
|
||||
Triangle Theorems
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Two essential theorems unlock almost every SAT triangle problem.
|
||||
The interactive tool below lets you drag vertices to verify both
|
||||
dynamically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-emerald-900">
|
||||
Core Triangle Rules
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-1">
|
||||
Triangle Sum Theorem
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-2 rounded text-emerald-700 font-bold mb-2">
|
||||
∠A + ∠B + ∠C = 180°
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">
|
||||
The three interior angles of any triangle always sum to
|
||||
exactly 180°. No exceptions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-1">
|
||||
Exterior Angle Theorem
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-2 rounded text-emerald-700 font-bold mb-2">
|
||||
∠ext = ∠A + ∠B
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">
|
||||
An exterior angle equals the sum of the two non-adjacent
|
||||
(remote) interior angles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Examples */}
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-100">
|
||||
<p className="font-bold text-emerald-800 mb-3">Worked Examples</p>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-emerald-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-emerald-800 mb-1">
|
||||
Example 1: Find a missing angle
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>Two angles are 47° and 83°. Find the third.</p>
|
||||
<p>
|
||||
∠C = 180° − 47° − 83° ={" "}
|
||||
<strong className="text-emerald-700">50°</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-emerald-800 mb-1">
|
||||
Example 2: Exterior angle
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
Two interior angles are 40° and 65°. Find the exterior
|
||||
angle at the third vertex.
|
||||
</p>
|
||||
<p>
|
||||
∠ext = 40° + 65° ={" "}
|
||||
<strong className="text-emerald-700">105°</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-emerald-800 mb-1">
|
||||
Example 3: Isosceles triangle
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
An isosceles triangle has vertex angle = 40°. Find the
|
||||
base angles.
|
||||
</p>
|
||||
<p>
|
||||
Each base angle = <Frac n="180° − 40°" d="2" /> ={" "}
|
||||
<Frac n="140°" d="2" /> ={" "}
|
||||
<strong className="text-emerald-700">70°</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Triangle Inequality */}
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-sky-900 mb-1">
|
||||
Triangle Inequality Theorem
|
||||
</p>
|
||||
<p className="text-slate-700 mb-2">
|
||||
Any two sides of a triangle must sum to more than the third
|
||||
side.
|
||||
</p>
|
||||
<div className="font-mono text-xs bg-white rounded p-2 text-slate-700">
|
||||
<p>a + b > c, a + c > b, b + c > a</p>
|
||||
<p className="mt-1">
|
||||
Example: Can a triangle have sides 3, 5, 9?
|
||||
</p>
|
||||
<p>3 + 5 = 8 < 9 → NO, not a valid triangle.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InteractiveTriangle />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-8 group flex items-center text-emerald-600 font-bold hover:text-emerald-800"
|
||||
>
|
||||
Next: Special Triangles{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Special Triangles */}
|
||||
<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">
|
||||
Special Right Triangles
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
These two triangle types appear constantly on the SAT. Memorize
|
||||
their side ratios so you can find any missing side without using
|
||||
the Pythagorean theorem.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-emerald-900">
|
||||
The Two Special Right Triangles
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-2">
|
||||
45° — 45° — 90°
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-3 rounded text-emerald-700 font-bold text-lg mb-2">
|
||||
Sides: 1 : 1 : √2
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Two equal legs. Hypotenuse = leg × √2.
|
||||
</p>
|
||||
<div className="font-mono text-xs bg-slate-50 rounded p-2 text-slate-700">
|
||||
<p>If leg = 5: hyp = 5√2</p>
|
||||
<p>
|
||||
If hyp = 8: leg = <Frac n="8" d="√2" /> = 4√2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-2">
|
||||
30° — 60° — 90°
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-3 rounded text-emerald-700 font-bold text-lg mb-2">
|
||||
Sides: 1 : √3 : 2
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Shortest leg opposite 30°. Hypotenuse = 2 × short leg.
|
||||
</p>
|
||||
<div className="font-mono text-xs bg-slate-50 rounded p-2 text-slate-700">
|
||||
<p>Short leg = 4: long leg = 4√3, hyp = 8</p>
|
||||
<p>Hyp = 10: short leg = 5, long leg = 5√3</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pythagorean Theorem */}
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-100">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Pythagorean Theorem & Common Triples
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-2 rounded text-emerald-700 font-bold text-lg mb-3">
|
||||
a² + b² = c²
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-3">
|
||||
c is always the hypotenuse (opposite the right angle). Memorize
|
||||
these Pythagorean triples — they appear frequently on the SAT:
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-xl border border-emerald-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-emerald-900 text-white">
|
||||
<th className="p-2 text-center">Triple (a, b, c)</th>
|
||||
<th className="p-2 text-center">Scaled Version</th>
|
||||
<th className="p-2 text-center">Verify</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-emerald-100">
|
||||
<tr className="bg-white text-center">
|
||||
<td className="p-2 font-bold">3, 4, 5</td>
|
||||
<td className="p-2 text-slate-600">6-8-10, 9-12-15</td>
|
||||
<td className="p-2 font-mono text-xs">9 + 16 = 25 ✓</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50 text-center">
|
||||
<td className="p-2 font-bold">5, 12, 13</td>
|
||||
<td className="p-2 text-slate-600">10-24-26</td>
|
||||
<td className="p-2 font-mono text-xs">
|
||||
25 + 144 = 169 ✓
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white text-center">
|
||||
<td className="p-2 font-bold">8, 15, 17</td>
|
||||
<td className="p-2 text-slate-600">—</td>
|
||||
<td className="p-2 font-mono text-xs">
|
||||
64 + 225 = 289 ✓
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50 text-center">
|
||||
<td className="p-2 font-bold">7, 24, 25</td>
|
||||
<td className="p-2 text-slate-600">—</td>
|
||||
<td className="p-2 font-mono text-xs">
|
||||
49 + 576 = 625 ✓
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked examples */}
|
||||
<div className="space-y-3">
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example: 30-60-90
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
An equilateral triangle has side length 10. Find the height.
|
||||
</p>
|
||||
<p>The height bisects it into two 30-60-90 triangles.</p>
|
||||
<p>Short leg (half the base) = 5</p>
|
||||
<p>
|
||||
Long leg (height) = 5√3 ≈{" "}
|
||||
<strong className="text-sky-800">8.66</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 rounded-xl p-4 border border-sky-200 text-sm">
|
||||
<p className="font-semibold text-sky-800 mb-2">
|
||||
Worked Example: Pythagorean Triple
|
||||
</p>
|
||||
<div className="font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
A right triangle has legs 9 and 12. Find the hypotenuse.
|
||||
</p>
|
||||
<p>Recognize: 9 and 12 are multiples of 3 and 4 (× 3).</p>
|
||||
<p>
|
||||
This is a 3-4-5 triple × 3: hypotenuse ={" "}
|
||||
<strong className="text-sky-800">15</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="group flex items-center text-emerald-600 font-bold hover:text-emerald-800"
|
||||
>
|
||||
Next: Polygons{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Polygons */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Polygons
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
Polygon angle rules extend triangle logic — any polygon can be
|
||||
divided into triangles, which is where the interior angle sum
|
||||
formula comes from.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-emerald-900">
|
||||
Polygon Angle Formulas
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-1">
|
||||
Interior Angle Sum
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-2 rounded text-emerald-700 font-bold mb-2">
|
||||
(n − 2) × 180°
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
n = number of sides. Triangle: 180° | Quadrilateral: 360° |
|
||||
Pentagon: 540° | Hexagon: 720°
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-200">
|
||||
<p className="font-bold text-emerald-900 mb-1">
|
||||
Exterior Angle Sum
|
||||
</p>
|
||||
<div className="font-mono text-center bg-emerald-50 py-2 rounded text-emerald-700 font-bold mb-2">
|
||||
Always = 360°
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
True for ALL convex polygons, regardless of n. Imagine walking
|
||||
around the polygon — you turn a full circle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reference table */}
|
||||
<div className="bg-white rounded-xl p-5 border border-emerald-100">
|
||||
<p className="font-bold text-emerald-800 mb-3">
|
||||
Quick Reference: Regular Polygons
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-xl border border-emerald-200">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-emerald-900 text-white">
|
||||
<th className="p-2 text-center">Polygon</th>
|
||||
<th className="p-2 text-center">Sides (n)</th>
|
||||
<th className="p-2 text-center">Interior Sum</th>
|
||||
<th className="p-2 text-center">Each Interior Angle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-emerald-100 text-center">
|
||||
<tr className="bg-white">
|
||||
<td className="p-2">Triangle</td>
|
||||
<td className="p-2">3</td>
|
||||
<td className="p-2">180°</td>
|
||||
<td className="p-2">60°</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-2">Quadrilateral</td>
|
||||
<td className="p-2">4</td>
|
||||
<td className="p-2">360°</td>
|
||||
<td className="p-2">90°</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-2">Pentagon</td>
|
||||
<td className="p-2">5</td>
|
||||
<td className="p-2">540°</td>
|
||||
<td className="p-2">108°</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-2">Hexagon</td>
|
||||
<td className="p-2">6</td>
|
||||
<td className="p-2">720°</td>
|
||||
<td className="p-2">120°</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-2">Octagon</td>
|
||||
<td className="p-2">8</td>
|
||||
<td className="p-2">1080°</td>
|
||||
<td className="p-2">135°</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formula for one angle */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-slate-800 mb-1">
|
||||
Each Interior Angle of a Regular n-gon
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-slate-700 font-bold mb-2">
|
||||
<Frac n="(n − 2) × 180°" d="n" />
|
||||
</div>
|
||||
<p className="text-slate-600 text-xs">
|
||||
Example: Regular hexagon → <Frac n="(6 − 2) × 180°" d="6" /> ={" "}
|
||||
<Frac n="720°" d="6" /> = <strong>120°</strong>
|
||||
</p>
|
||||
<p className="text-slate-600 text-xs mt-1">
|
||||
Example: Regular octagon → <Frac n="(8 − 2) × 180°" d="8" /> ={" "}
|
||||
<Frac n="1080°" d="8" /> = <strong>135°</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PolygonWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(4)}
|
||||
className="mt-12 group flex items-center text-emerald-600 font-bold hover:text-emerald-800"
|
||||
>
|
||||
Next: Practice{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 5: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[4] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="p-3 bg-emerald-100 rounded-full">
|
||||
<BookOpen className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900">
|
||||
Practice Time
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{ANGLES_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="bg-slate-200 text-slate-600 text-xs font-bold px-2 py-1 rounded uppercase">
|
||||
Question {idx + 1}
|
||||
</span>
|
||||
</div>
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="p-8 bg-emerald-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-emerald-900 font-bold rounded-full hover:bg-emerald-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinesAnglesLesson;
|
||||
365
src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx
Normal file
365
src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx
Normal file
@ -0,0 +1,365 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Triangle,
|
||||
Layers,
|
||||
Scale,
|
||||
Ruler,
|
||||
Hash,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import InteractiveTransversal from "../../../components/lessons/InteractiveTransversal";
|
||||
import SimilarityWidget from "../../../components/lessons/SimilarityWidget";
|
||||
import SimilarityTestsWidget from "../../../components/lessons/SimilarityTestsWidget";
|
||||
import ScaleFactorWidget from "../../../components/lessons/ScaleFactorWidget";
|
||||
import {
|
||||
LINES_ANGLES_EASY,
|
||||
LINES_ANGLES_MEDIUM,
|
||||
} from "../../../data/math/lines-angles-triangles";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Angle Relationships", icon: ArrowRight },
|
||||
{ title: "Parallel Lines & Transversals", icon: Layers },
|
||||
{ title: "Triangle Properties", icon: Triangle },
|
||||
{ title: "Congruence & Similarity", icon: Scale },
|
||||
{ title: "Pythagorean Theorem", icon: Hash },
|
||||
{ title: "Special Triangles", icon: Ruler },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function LinesAnglesTrianglesLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Lines, Angles & Triangles"
|
||||
sections={SECTIONS}
|
||||
color="emerald"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Angle Relationships
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Complementary
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">Sum = 90°</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Supplementary
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">Sum = 180°</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Vertical Angles
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Opposite angles are equal
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">Linear Pair</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Adjacent angles on a line = 180°
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Supplementary" color="emerald">
|
||||
<p>Two supplementary angles: (3x + 10)° and (2x − 5)°</p>
|
||||
<p className="text-slate-500">
|
||||
3x + 10 + 2x − 5 = 180 → 5x + 5 = 180 → x = 35
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">Angles: 115° and 65°</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Parallel Lines & Transversals
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
When a transversal crosses parallel lines, it creates 8 angles with
|
||||
special relationships:
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm text-slate-700">
|
||||
<p>
|
||||
• <strong>Corresponding angles</strong> are equal (same position
|
||||
at each intersection)
|
||||
</p>
|
||||
<p>
|
||||
• <strong>Alternate interior angles</strong> are equal (opposite
|
||||
sides, between lines)
|
||||
</p>
|
||||
<p>
|
||||
• <strong>Alternate exterior angles</strong> are equal (opposite
|
||||
sides, outside lines)
|
||||
</p>
|
||||
<p>
|
||||
• <strong>Co-interior</strong> (same-side interior) are
|
||||
supplementary (sum = 180°)
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<InteractiveTransversal />
|
||||
</div>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Lines l ∥ m cut by transversal. One angle = 65°</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">
|
||||
All 8 angles are either 65° or 115°
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Triangle Properties
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Key properties every SAT student must know:
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm text-slate-700">
|
||||
<p>
|
||||
• <strong>Angle sum:</strong> Interior angles always sum to 180°
|
||||
</p>
|
||||
<p>
|
||||
• <strong>Exterior angle:</strong> Equals the sum of the two
|
||||
non-adjacent interior angles
|
||||
</p>
|
||||
<p>
|
||||
• <strong>Triangle inequality:</strong> Sum of any two sides >
|
||||
third side
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-emerald-100 text-center text-sm">
|
||||
<p className="font-bold text-emerald-800">Equilateral</p>
|
||||
<p className="text-xs text-slate-500">All sides equal, all 60°</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-emerald-100 text-center text-sm">
|
||||
<p className="font-bold text-emerald-800">Isosceles</p>
|
||||
<p className="text-xs text-slate-500">Two equal sides/angles</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-emerald-100 text-center text-sm">
|
||||
<p className="font-bold text-emerald-800">Right</p>
|
||||
<p className="text-xs text-slate-500">One 90° angle</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Angle Sum" color="emerald">
|
||||
<p>Triangle angles: x, 2x, 3x</p>
|
||||
<p className="text-slate-500">
|
||||
x + 2x + 3x = 180 → 6x = 180 → x = 30
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">
|
||||
Angles: 30°, 60°, 90° — a special right triangle!
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Congruence & Similarity
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Congruent</strong> = same shape AND size.{" "}
|
||||
<strong>Similar</strong> = same shape, possibly different size
|
||||
(corresponding sides are proportional).
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<p className="font-bold text-blue-800 mb-1">Congruence Tests</p>
|
||||
<p className="text-sm text-slate-700">SSS, SAS, ASA, AAS</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-xl p-4">
|
||||
<p className="font-bold text-violet-800 mb-1">Similarity Tests</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
AA (two angles equal), SSS ratio, SAS ratio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Similar Triangles" color="emerald">
|
||||
<p>Triangle A: sides 3, 4, 5. Triangle B: sides 6, 8, 10.</p>
|
||||
<p className="text-slate-500">Ratios: 6÷3 = 8÷4 = 10÷5 = 2</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-emerald-700">
|
||||
Similar with scale factor 2
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Explore Similarity Tests
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Drag vertex B to reshape the triangle. Switch between AA, SAS, and SSS
|
||||
to see what each test checks.
|
||||
</p>
|
||||
<SimilarityTestsWidget />
|
||||
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Scale Factor Effects
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
See how the scale factor k affects lengths, areas, and volumes.
|
||||
</p>
|
||||
<ScaleFactorWidget />
|
||||
|
||||
<div className="mt-8">
|
||||
<SimilarityWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Pythagorean Theorem
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
In a <strong>right triangle</strong>, the square of the hypotenuse
|
||||
equals the sum of squares of the legs.
|
||||
</p>
|
||||
<FormulaBox>a² + b² = c² (c is the hypotenuse)</FormulaBox>
|
||||
<div className="mt-4 bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-900 text-sm mb-2">
|
||||
Common Pythagorean Triples
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-slate-700">
|
||||
<span className="font-mono bg-white rounded-lg px-3 py-1 text-center">
|
||||
3-4-5
|
||||
</span>
|
||||
<span className="font-mono bg-white rounded-lg px-3 py-1 text-center">
|
||||
5-12-13
|
||||
</span>
|
||||
<span className="font-mono bg-white rounded-lg px-3 py-1 text-center">
|
||||
8-15-17
|
||||
</span>
|
||||
<span className="font-mono bg-white rounded-lg px-3 py-1 text-center">
|
||||
7-24-25
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
And their multiples: 6-8-10, 9-12-15, 10-24-26, etc.
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Right triangle: legs 6 and 8</p>
|
||||
<p className="text-slate-500">
|
||||
c² = 36 + 64 = 100 →{" "}
|
||||
<strong className="text-emerald-700">c = 10</strong>
|
||||
</p>
|
||||
<p className="text-slate-500">(This is 2 × the 3-4-5 triple)</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Recognize multiples of Pythagorean triples to save time. If you
|
||||
see 15-20-?, it's 5 × (3-4-5), so the answer is 25.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Special Right Triangles
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Two special right triangles appear <strong>constantly</strong> on
|
||||
the SAT:
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-800 mb-2">
|
||||
45-45-90 Triangle
|
||||
</p>
|
||||
<FormulaBox>Sides: x : x : x√2</FormulaBox>
|
||||
<p className="text-sm text-slate-600 mt-2">
|
||||
Isosceles right triangle. Legs are equal.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-800 mb-2">
|
||||
30-60-90 Triangle
|
||||
</p>
|
||||
<FormulaBox>Sides: x : x√3 : 2x</FormulaBox>
|
||||
<p className="text-sm text-slate-600 mt-2">
|
||||
Short leg opposite 30°. Hypotenuse = 2 × short leg.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: 45-45-90" color="emerald">
|
||||
<p>Leg = 7</p>
|
||||
<p className="text-slate-500">
|
||||
Hypotenuse ={" "}
|
||||
<strong className="text-emerald-700">7√2 ≈ 9.90</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: 30-60-90" color="emerald">
|
||||
<p>Hypotenuse = 10</p>
|
||||
<p className="text-slate-500">
|
||||
Short leg = 5, long leg ={" "}
|
||||
<strong className="text-emerald-700">5√3 ≈ 8.66</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
The side opposite the 30° angle is always the shortest (half the
|
||||
hypotenuse). The side opposite 60° is the short leg × √3.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 7 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{LINES_ANGLES_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
{LINES_ANGLES_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
267
src/pages/student/lessons/NonlinearEq1VarLesson.tsx
Normal file
267
src/pages/student/lessons/NonlinearEq1VarLesson.tsx
Normal file
@ -0,0 +1,267 @@
|
||||
import React from "react";
|
||||
import { Layers, Hash, Target, Zap, RotateCcw, BookOpen } from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import DiscriminantWidget from "../../../components/lessons/DiscriminantWidget";
|
||||
import CompletingSquareWidget from "../../../components/lessons/CompletingSquareWidget";
|
||||
import RadicalSolutionWidget from "../../../components/lessons/RadicalSolutionWidget";
|
||||
import {
|
||||
NONLINEAR_EQ_EASY,
|
||||
NONLINEAR_EQ_MEDIUM,
|
||||
} from "../../../data/math/nonlinear-equations";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Factoring Quadratics", icon: Layers },
|
||||
{ title: "Completing the Square", icon: Hash },
|
||||
{ title: "Quadratic Formula", icon: Target },
|
||||
{ title: "Polynomial Equations", icon: Zap },
|
||||
{ title: "Radical & Rational Equations", icon: RotateCcw },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function NonlinearEq1VarLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Nonlinear Equations in One Variable"
|
||||
sections={SECTIONS}
|
||||
color="violet"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Factoring Quadratics */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Factoring Quadratics
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
To solve a quadratic by factoring: set it equal to zero, factor,
|
||||
then apply the <strong>Zero Product Property</strong> — if ab = 0,
|
||||
then a = 0 or b = 0.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
If (expression₁)(expression₂) = 0, then expression₁ = 0 or
|
||||
expression₂ = 0
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Simple Factoring" color="violet">
|
||||
<p>x² − 5x + 6 = 0</p>
|
||||
<p className="text-slate-500">(x − 2)(x − 3) = 0</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = 2 or x = 3</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Leading Coefficient ≠ 1" color="violet">
|
||||
<p>2x² + x − 6 = 0</p>
|
||||
<p className="text-slate-500">(2x − 3)(x + 2) = 0</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">
|
||||
x = <Frac n="3" d="2" /> or x = −2
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Always check if you can factor out a GCF first. For example, 6x² −
|
||||
12x = 0 → 6x(x − 2) = 0 → x = 0 or x = 2.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Completing the Square */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Completing the Square
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Rewrite ax² + bx + c into <strong>vertex form</strong> a(x − h)² +
|
||||
k. This reveals the vertex and lets you solve any quadratic.
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm">
|
||||
<p>1. Move the constant to the other side</p>
|
||||
<p>2. If a ≠ 1, divide everything by a</p>
|
||||
<p>3. Take half of b, square it, add to both sides</p>
|
||||
<p>4. Factor the perfect square trinomial</p>
|
||||
</div>
|
||||
<FormulaBox>x² + bx + (b ÷ 2)² = (x + b ÷ 2)²</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="violet">
|
||||
<p>x² + 6x + 2 = 0</p>
|
||||
<p className="text-slate-500">x² + 6x = −2</p>
|
||||
<p className="text-slate-500">x² + 6x + 9 = −2 + 9 = 7</p>
|
||||
<p className="text-slate-500">(x + 3)² = 7</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = −3 ± √7</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<CompletingSquareWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Quadratic Formula */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Quadratic Formula & Discriminant
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The quadratic formula works for <strong>every</strong> quadratic
|
||||
equation ax² + bx + c = 0.
|
||||
</p>
|
||||
<FormulaBox>x = (−b ± √(b² − 4ac)) ÷ 2a</FormulaBox>
|
||||
<p className="text-slate-700 mt-4">
|
||||
The <strong>discriminant</strong> (b² − 4ac) tells you the number of
|
||||
real solutions:
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 mt-3">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-emerald-700">b² − 4ac > 0</p>
|
||||
<p className="text-xs text-slate-500">2 real solutions</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700">b² − 4ac = 0</p>
|
||||
<p className="text-xs text-slate-500">1 repeated solution</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-rose-700">b² − 4ac < 0</p>
|
||||
<p className="text-xs text-slate-500">No real solutions</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="violet">
|
||||
<p>2x² − 3x − 5 = 0 → a = 2, b = −3, c = −5</p>
|
||||
<p className="text-slate-500">
|
||||
Discriminant: (−3)² − 4(2)(−5) = 9 + 40 = 49
|
||||
</p>
|
||||
<p className="text-slate-500">x = (3 ± 7) ÷ 4</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">
|
||||
x = <Frac n="5" d="2" /> or x = −1
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<DiscriminantWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Polynomial Equations */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Polynomial Equations
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
For higher-degree polynomials, try{" "}
|
||||
<strong>factoring out a GCF</strong> first, then use known
|
||||
identities or factoring by grouping.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Factor Out GCF" color="violet">
|
||||
<p>x³ − 4x = 0</p>
|
||||
<p className="text-slate-500">x(x² − 4) = 0</p>
|
||||
<p className="text-slate-500">x(x + 2)(x − 2) = 0</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = 0, x = −2, or x = 2</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Substitution" color="violet">
|
||||
<p>x⁴ − 5x² + 4 = 0</p>
|
||||
<p className="text-slate-500">Let u = x² → u² − 5u + 4 = 0</p>
|
||||
<p className="text-slate-500">
|
||||
(u − 1)(u − 4) = 0 → u = 1 or u = 4
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = ±1 or x = ±2</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
On the SAT, always check if you can factor out a common term
|
||||
first. It's the fastest path to simplification.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Radical & Rational */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Radical & Rational Equations
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Radical equations:</strong> Isolate the radical, then square
|
||||
both sides. <strong>Rational equations:</strong> Find the LCD,
|
||||
multiply through, then solve. In both cases, you{" "}
|
||||
<strong>must check for extraneous solutions</strong>.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Radical Equation" color="violet">
|
||||
<p>√(2x + 3) = x</p>
|
||||
<p className="text-slate-500">Square both sides: 2x + 3 = x²</p>
|
||||
<p className="text-slate-500">x² − 2x − 3 = 0 → (x − 3)(x + 1) = 0</p>
|
||||
<p className="text-slate-500">Check x = 3: √9 = 3 ✓</p>
|
||||
<p className="text-slate-500">Check x = −1: √1 = 1 ≠ −1 ✗</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = 3 only</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Rational Equation" color="violet">
|
||||
<p>
|
||||
<Frac n="3" d="x" /> + <Frac n="1" d="2" /> = <Frac n="5" d="x" />
|
||||
</p>
|
||||
<p className="text-slate-500">Multiply by 2x: 6 + x = 10 → x = 4</p>
|
||||
<p className="text-slate-500">
|
||||
Check: x ≠ 0 ✓ →{" "}
|
||||
<strong className="text-violet-700">x = 4</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<RadicalSolutionWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
ALWAYS check your solutions in the original equation. Squaring
|
||||
both sides can introduce extraneous solutions, and rational
|
||||
equations can have excluded values.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{NONLINEAR_EQ_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
{NONLINEAR_EQ_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
414
src/pages/student/lessons/NonlinearFunctionsLesson.tsx
Normal file
414
src/pages/student/lessons/NonlinearFunctionsLesson.tsx
Normal file
@ -0,0 +1,414 @@
|
||||
import {
|
||||
TrendingUp,
|
||||
BarChart,
|
||||
Zap,
|
||||
RotateCcw,
|
||||
Layers,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import ParabolaWidget from "../../../components/lessons/ParabolaWidget";
|
||||
import PolynomialBehaviorWidget from "../../../components/lessons/PolynomialBehaviorWidget";
|
||||
import ExponentialExplorer from "../../../components/lessons/ExponentialExplorer";
|
||||
import RemainderTheoremWidget from "../../../components/lessons/RemainderTheoremWidget";
|
||||
import GrowthComparisonWidget from "../../../components/lessons/GrowthComparisonWidget";
|
||||
import {
|
||||
NONLINEAR_FUNC_EASY,
|
||||
NONLINEAR_FUNC_MEDIUM,
|
||||
} from "../../../data/math/nonlinear-functions";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Quadratic Functions", icon: TrendingUp },
|
||||
{ title: "Polynomial Behavior", icon: BarChart },
|
||||
{ title: "Exponential Growth & Decay", icon: Zap },
|
||||
{ title: "Radical & Rational Functions", icon: RotateCcw },
|
||||
{ title: "Transformations", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function NonlinearFunctionsLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Nonlinear Functions"
|
||||
sections={SECTIONS}
|
||||
color="violet"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Quadratic Functions */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Quadratic Functions
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A quadratic function can be written in three forms, each revealing
|
||||
different information:
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-xl p-3">
|
||||
<p className="font-bold text-violet-800 text-sm">Standard Form</p>
|
||||
<p className="font-mono text-sm text-slate-700">
|
||||
f(x) = ax² + bx + c
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Shows y-intercept (c)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-xl p-3">
|
||||
<p className="font-bold text-violet-800 text-sm">Vertex Form</p>
|
||||
<p className="font-mono text-sm text-slate-700">
|
||||
f(x) = a(x − h)² + k
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">Shows vertex (h, k)</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-xl p-3">
|
||||
<p className="font-bold text-violet-800 text-sm">Factored Form</p>
|
||||
<p className="font-mono text-sm text-slate-700">
|
||||
f(x) = a(x − r₁)(x − r₂)
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Shows x-intercepts (r₁, r₂)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<FormulaBox>Vertex x-coordinate: x = −b ÷ 2a</FormulaBox>
|
||||
<p className="text-slate-700 text-sm mt-2">
|
||||
If a > 0 → opens up (minimum). If a < 0 → opens down
|
||||
(maximum).
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Find the Vertex" color="violet">
|
||||
<p>f(x) = 2x² − 8x + 3</p>
|
||||
<p className="text-slate-500">x = −(−8) ÷ (2 × 2) = 8 ÷ 4 = 2</p>
|
||||
<p className="text-slate-500">f(2) = 2(4) − 16 + 3 = −5</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">
|
||||
Vertex: (2, −5), minimum value = −5
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ParabolaWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Polynomial Behavior */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Polynomial Behavior
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The <strong>degree</strong> determines the maximum number of turning
|
||||
points (degree − 1). The <strong>leading coefficient</strong> and
|
||||
degree determine end behavior.
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-100 text-violet-900">
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Degree
|
||||
</th>
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Leading Coeff
|
||||
</th>
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Left End
|
||||
</th>
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Right End
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">Even</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Positive
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↑ Up</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↑ Up</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2">Even</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Negative
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↓ Down</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↓ Down</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">Odd</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Positive
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↓ Down</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↑ Up</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2">Odd</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Negative
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↑ Up</td>
|
||||
<td className="border border-slate-200 px-3 py-2">↓ Down</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="violet">
|
||||
<p>f(x) = −2x³ + 5x</p>
|
||||
<p className="text-slate-500">
|
||||
Degree 3 (odd), negative leading coefficient
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">Rises left, falls right</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<PolynomialBehaviorWidget />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Remainder Theorem Explorer
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Slide the divisor value to see how the remainder changes — when it
|
||||
hits zero, you've found a root!
|
||||
</p>
|
||||
<RemainderTheoremWidget />
|
||||
</div>
|
||||
|
||||
{/* Section 3: Exponential Growth & Decay */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Exponential Growth & Decay
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Exponential functions change by a constant <strong>factor</strong>{" "}
|
||||
(not a constant amount like linear functions).
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>
|
||||
Growth: f(x) = a(1 + r)<sup>x</sup> where b = 1 + r > 1
|
||||
</FormulaBox>
|
||||
<FormulaBox>
|
||||
Decay: f(x) = a(1 − r)<sup>x</sup> where b = 1 − r < 1
|
||||
</FormulaBox>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Growth (b > 1)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Population, compound interest, bacteria
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3">
|
||||
<p className="font-bold text-rose-800 text-sm">
|
||||
Decay (0 < b < 1)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Depreciation, radioactive decay, cooling
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Growth" color="violet">
|
||||
<p>Population starts at 500, grows 8% per year</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">
|
||||
P(t) = 500(1.08)<sup>t</sup>
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Decay" color="violet">
|
||||
<p>Car worth $25,000 depreciates 15% per year</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">
|
||||
V(t) = 25000(0.85)<sup>t</sup>
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<ExponentialExplorer />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mt-10 mb-3">
|
||||
Linear vs Exponential Growth
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Compare how linear and exponential growth diverge over time.
|
||||
</p>
|
||||
<GrowthComparisonWidget />
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
"Doubles every 3 years" means f(t) = a × 2<sup>t÷3</sup>. "Halves
|
||||
every 5 hours" means f(t) = a × (0.5)<sup>t÷5</sup>.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Radical & Rational Functions */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Radical & Rational Functions
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Radical functions:</strong> f(x) = √(expression). Domain
|
||||
requires expression ≥ 0.
|
||||
<br />
|
||||
<strong>Rational functions:</strong> f(x) = p(x) ÷ q(x). Domain
|
||||
excludes where q(x) = 0.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-violet-100 text-sm">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Vertical Asymptotes
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
Where denominator = 0 (and numerator ≠ 0)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-violet-100 text-sm">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Horizontal Asymptotes
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
Compare degrees of numerator and denominator
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Rational Function" color="violet">
|
||||
<p>f(x) = (x + 2) ÷ (x − 3)</p>
|
||||
<p className="text-slate-500">Vertical asymptote: x = 3</p>
|
||||
<p className="text-slate-500">
|
||||
Horizontal asymptote: y = 1 (same degree → ratio of leading
|
||||
coefficients)
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Transformations */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Function Transformations
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Transformations modify the graph of a parent function.
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-100 text-violet-900">
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Notation
|
||||
</th>
|
||||
<th className="border border-violet-300 px-3 py-2 font-bold">
|
||||
Transformation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
f(x) + k
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Shift up k units
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
f(x) − k
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Shift down k units
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
f(x − h)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Shift right h units
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
f(x + h)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Shift left h units
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
−f(x)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Reflect over x-axis
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
f(−x)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Reflect over y-axis
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-mono">
|
||||
af(x)
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Vertical stretch (a > 1) or compress (0 < a < 1)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
"Inside" changes (affecting x) go the <strong>opposite</strong>{" "}
|
||||
direction. "Outside" changes (affecting y) go the{" "}
|
||||
<strong>same</strong> direction.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{NONLINEAR_FUNC_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
{NONLINEAR_FUNC_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
271
src/pages/student/lessons/OneVariableDataLesson.tsx
Normal file
271
src/pages/student/lessons/OneVariableDataLesson.tsx
Normal file
@ -0,0 +1,271 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BarChart,
|
||||
Box,
|
||||
Calculator,
|
||||
Ruler,
|
||||
TrendingUp,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import HistogramBuilderWidget from "../../../components/lessons/HistogramBuilderWidget";
|
||||
import BoxPlotAnatomyWidget from "../../../components/lessons/BoxPlotAnatomyWidget";
|
||||
import FrequencyMeanWidget from "../../../components/lessons/FrequencyMeanWidget";
|
||||
import {
|
||||
ONE_VAR_DATA_EASY,
|
||||
ONE_VAR_DATA_MEDIUM,
|
||||
} from "../../../data/math/one-variable-data";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Histograms & Dot Plots", icon: BarChart },
|
||||
{ title: "Box Plots", icon: Box },
|
||||
{ title: "Mean, Median, Mode", icon: Calculator },
|
||||
{ title: "Range, IQR & Spread", icon: Ruler },
|
||||
{ title: "Standard Deviation & Outliers", icon: TrendingUp },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function OneVariableDataLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="One-Variable Data: Distributions & Measures"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Histograms & Dot Plots
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Histograms</strong> group data into bins/intervals and show
|
||||
frequency with bar heights. <strong>Dot plots</strong> show
|
||||
individual values as stacked dots. Both reveal the{" "}
|
||||
<strong>shape</strong> of the distribution.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700 text-sm">Symmetric</p>
|
||||
<p className="text-xs text-slate-500">Mirror image</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700 text-sm">Skewed Left</p>
|
||||
<p className="text-xs text-slate-500">Tail to the left</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700 text-sm">Skewed Right</p>
|
||||
<p className="text-xs text-slate-500">Tail to the right</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700 text-sm">Bimodal</p>
|
||||
<p className="text-xs text-slate-500">Two peaks</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<HistogramBuilderWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Box Plots (Box-and-Whisker)
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A box plot displays the <strong>five-number summary</strong>:
|
||||
minimum, Q1, median, Q3, maximum.
|
||||
</p>
|
||||
<div className="mt-4 bg-white/60 rounded-xl border border-amber-200 p-4">
|
||||
<div className="flex items-center justify-between text-sm font-mono text-slate-700">
|
||||
<span>Min</span>
|
||||
<span>Q1</span>
|
||||
<span>Median</span>
|
||||
<span>Q3</span>
|
||||
<span>Max</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-slate-500 text-center">
|
||||
← 25% →|← 25% →|← 25% →|← 25% →
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mt-3">
|
||||
<strong>IQR</strong> = Q3 − Q1 (the "box" width, containing the
|
||||
middle 50%)
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<BoxPlotAnatomyWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Outliers are typically defined as values below Q1 − 1.5 × IQR or
|
||||
above Q3 + 1.5 × IQR.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Mean, Median, Mode
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Three measures of center:
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
|
||||
<p className="font-bold text-amber-800 text-sm">Mean</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Sum ÷ count. Affected by outliers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
|
||||
<p className="font-bold text-amber-800 text-sm">Median</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Middle value. Resistant to outliers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
|
||||
<p className="font-bold text-amber-800 text-sm">Mode</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Most frequent value. Can have none or many.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<FormulaBox>Mean = Σx ÷ n</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>Data: 3, 5, 5, 7, 10</p>
|
||||
<p className="text-slate-500">Mean = (3 + 5 + 5 + 7 + 10) ÷ 5 = 6</p>
|
||||
<p className="text-slate-500">Median = 5 (middle value)</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">Mode = 5 (appears most)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<FrequencyMeanWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
For skewed distributions, use <strong>median</strong> (resistant
|
||||
to outliers). For symmetric distributions, mean ≈ median.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Range, IQR & Spread
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Measures of spread tell you how spread out the data is.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>Range = Maximum − Minimum</FormulaBox>
|
||||
<FormulaBox>IQR = Q3 − Q1 (middle 50% of data)</FormulaBox>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100 text-sm">
|
||||
<p className="font-bold text-amber-800 mb-1">Range</p>
|
||||
<p className="text-slate-600">
|
||||
Uses only two values. Very sensitive to outliers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100 text-sm">
|
||||
<p className="font-bold text-amber-800 mb-1">IQR</p>
|
||||
<p className="text-slate-600">
|
||||
Resistant to outliers. Better measure of typical spread.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>Data: 2, 4, 6, 8, 10, 12, 14</p>
|
||||
<p className="text-slate-500">Range = 14 − 2 = 12</p>
|
||||
<p className="text-slate-500">
|
||||
Q1 = 4, Q3 = 12 →{" "}
|
||||
<strong className="text-amber-700">IQR = 8</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Standard Deviation & Outliers
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Standard deviation</strong> (SD) measures the average
|
||||
distance from the mean. You won't calculate it on the SAT, but you
|
||||
need to understand it.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">Small SD</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Data clustered near the mean
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3">
|
||||
<p className="font-bold text-rose-800 text-sm">Large SD</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Data spread far from the mean
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 text-sm mb-2">
|
||||
Effects of Outliers
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Outliers <strong>pull the mean</strong> toward them,{" "}
|
||||
<strong>increase</strong> the SD and range, but have{" "}
|
||||
<strong>little effect</strong> on the median and IQR.
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Adding a constant to all values shifts the mean but{" "}
|
||||
<strong>doesn't change</strong> the SD. Multiplying all values by a
|
||||
constant multiplies <strong>both</strong> the mean and SD by that
|
||||
constant.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{ONE_VAR_DATA_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{ONE_VAR_DATA_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
240
src/pages/student/lessons/PercentagesLesson.tsx
Normal file
240
src/pages/student/lessons/PercentagesLesson.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
import {
|
||||
Percent,
|
||||
TrendingUp,
|
||||
ArrowRight,
|
||||
DollarSign,
|
||||
Layers,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import PercentChangeWidget from "../../../components/lessons/PercentChangeWidget";
|
||||
import MultiStepPercentWidget from "../../../components/lessons/MultiStepPercentWidget";
|
||||
import {
|
||||
PERCENTAGES_EASY,
|
||||
PERCENTAGES_MEDIUM,
|
||||
} from "../../../data/math/percentages";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Percent Basics", icon: Percent },
|
||||
{ title: "Percent Change", icon: TrendingUp },
|
||||
{ title: "Multi-Step Percent", icon: ArrowRight },
|
||||
{ title: "Markup & Discount", icon: DollarSign },
|
||||
{ title: "Simple & Compound Interest", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function PercentagesLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Percentages"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Percent Basics
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Percent means "per hundred." Three types of percent problems: find
|
||||
the <strong>part</strong>, find the <strong>whole</strong>, find the{" "}
|
||||
<strong>percent</strong>.
|
||||
</p>
|
||||
<FormulaBox>Percent = (Part ÷ Whole) × 100</FormulaBox>
|
||||
<FormulaBox>Part = Percent × Whole</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Find the Part" color="amber">
|
||||
<p>What is 35% of 80?</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">0.35 × 80 = 28</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Find the Percent" color="amber">
|
||||
<p>42 is what percent of 120?</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">(42 ÷ 120) × 100 = 35%</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Percent Change
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Percent change measures how much a value has increased or decreased
|
||||
relative to the original.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
Percent Change = (New − Original) ÷ Original × 100
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Percent Increase" color="amber">
|
||||
<p>Price goes from $80 to $100</p>
|
||||
<p className="text-slate-500">
|
||||
(100 − 80) ÷ 80 × 100 ={" "}
|
||||
<strong className="text-amber-700">25% increase</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Percent Decrease" color="amber">
|
||||
<p>Population drops from 500 to 425</p>
|
||||
<p className="text-slate-500">
|
||||
(500 − 425) ÷ 500 × 100 ={" "}
|
||||
<strong className="text-amber-700">15% decrease</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<PercentChangeWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Multi-Step Percent Problems
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Successive percent changes are <strong>multiplicative</strong>, not
|
||||
additive. A 20% increase followed by a 20% decrease does NOT return
|
||||
to the original!
|
||||
</p>
|
||||
<div className="mt-3 bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="text-sm text-slate-700">
|
||||
<strong>Multiplier method:</strong> Increase 20% = ×1.20. Decrease
|
||||
15% = ×0.85. Chain multipliers together.
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Why They Don't Cancel" color="amber">
|
||||
<p>$100 → increase 20% → $120 → decrease 20% → $96</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-red-600">NOT $100!</strong> Because 20% of
|
||||
120 ≠ 20% of 100.
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Chain Multipliers" color="amber">
|
||||
<p>$200 marked up 30%, then discounted 10%</p>
|
||||
<p className="text-slate-500">
|
||||
200 × 1.30 × 0.90 ={" "}
|
||||
<strong className="text-amber-700">$234</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<MultiStepPercentWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
Successive percent changes are multiplicative! A 50% increase then
|
||||
50% decrease gives ×1.5 × 0.5 = ×0.75, a 25% net decrease.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Markup & Discount
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Markup</strong> = percent increase from cost to selling
|
||||
price. <strong>Discount</strong> = percent decrease from original to
|
||||
sale price. Tax is added after.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Markup" color="amber">
|
||||
<p>Store buys item for $40, marks up 60%</p>
|
||||
<p className="text-slate-500">
|
||||
$40 × 1.60 ={" "}
|
||||
<strong className="text-amber-700">$64 selling price</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Discount + Tax" color="amber">
|
||||
<p>$85 item with 20% discount, then 8% tax</p>
|
||||
<p className="text-slate-500">$85 × 0.80 = $68 (after discount)</p>
|
||||
<p className="text-slate-500">
|
||||
$68 × 1.08 ={" "}
|
||||
<strong className="text-amber-700">$73.44 (final price)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Simple & Compound Interest
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Simple interest</strong> is calculated only on the
|
||||
principal. <strong>Compound interest</strong> earns interest on
|
||||
interest.
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>Simple: I = P × r × t</FormulaBox>
|
||||
<FormulaBox>
|
||||
Compound: A = P(1 + r ÷ n)<sup>nt</sup>
|
||||
</FormulaBox>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
P = principal, r = annual rate, t = time in years, n = compounds per
|
||||
year
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Simple Interest" color="amber">
|
||||
<p>$1,000 at 5% for 3 years</p>
|
||||
<p className="text-slate-500">
|
||||
I = 1000 × 0.05 × 3 ={" "}
|
||||
<strong className="text-amber-700">$150</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Compound Interest" color="amber">
|
||||
<p>$1,000 at 5% compounded annually for 3 years</p>
|
||||
<p className="text-slate-500">
|
||||
A = 1000(1.05)³ ={" "}
|
||||
<strong className="text-amber-700">$1,157.63</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{PERCENTAGES_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{PERCENTAGES_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
480
src/pages/student/lessons/PolynomialFunctionsLesson.tsx
Normal file
480
src/pages/student/lessons/PolynomialFunctionsLesson.tsx
Normal file
@ -0,0 +1,480 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, TrendingUp, Grid } from "lucide-react";
|
||||
import PolynomialBehaviorWidget from "../../../components/lessons/PolynomialBehaviorWidget";
|
||||
import MultiplicityWidget from "../../../components/lessons/MultiplicityWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { ADV_POLYNOMIAL_QUIZ } from "../../../utils/constants";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const PolynomialFunctionsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-violet-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-violet-600 text-white" : isPast ? "bg-violet-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-violet-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="End Behavior" icon={TrendingUp} />
|
||||
<SectionMarker index={1} title="Zeros & Multiplicity" icon={Grid} />
|
||||
<SectionMarker index={2} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: End Behavior */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
End Behavior of Polynomials
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
As x grows very large (→ +∞) or very negative (→ −∞), a
|
||||
polynomial's value is dominated by its{" "}
|
||||
<strong>leading term</strong> — the term with the highest power.
|
||||
To predict end behavior, you only need to look at two things: the{" "}
|
||||
<strong>degree</strong> (even or odd) and the{" "}
|
||||
<strong>sign of the leading coefficient</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* End Behavior Table */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
The Four End Behavior Rules
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-200 text-violet-900">
|
||||
<th className="p-3 rounded-tl-lg font-bold text-left">
|
||||
Degree
|
||||
</th>
|
||||
<th className="p-3 font-bold text-left">
|
||||
Leading Coefficient
|
||||
</th>
|
||||
<th className="p-3 font-bold text-left">As x → −∞</th>
|
||||
<th className="p-3 rounded-tr-lg font-bold text-left">
|
||||
As x → +∞
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="bg-white border-b border-violet-100">
|
||||
<td className="p-3 font-bold text-violet-700">
|
||||
Even (2, 4, 6…)
|
||||
</td>
|
||||
<td className="p-3 text-green-700 font-semibold">
|
||||
Positive (+)
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">y → +∞ (rises left)</td>
|
||||
<td className="p-3 text-slate-600">y → +∞ (rises right)</td>
|
||||
</tr>
|
||||
<tr className="bg-violet-50 border-b border-violet-100">
|
||||
<td className="p-3 font-bold text-violet-700">
|
||||
Even (2, 4, 6…)
|
||||
</td>
|
||||
<td className="p-3 text-red-700 font-semibold">
|
||||
Negative (−)
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">y → −∞ (falls left)</td>
|
||||
<td className="p-3 text-slate-600">y → −∞ (falls right)</td>
|
||||
</tr>
|
||||
<tr className="bg-white border-b border-violet-100">
|
||||
<td className="p-3 font-bold text-violet-700">
|
||||
Odd (1, 3, 5…)
|
||||
</td>
|
||||
<td className="p-3 text-green-700 font-semibold">
|
||||
Positive (+)
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">y → −∞ (falls left)</td>
|
||||
<td className="p-3 text-slate-600">y → +∞ (rises right)</td>
|
||||
</tr>
|
||||
<tr className="bg-violet-50">
|
||||
<td className="p-3 font-bold text-violet-700">
|
||||
Odd (1, 3, 5…)
|
||||
</td>
|
||||
<td className="p-3 text-red-700 font-semibold">
|
||||
Negative (−)
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">y → +∞ (rises left)</td>
|
||||
<td className="p-3 text-slate-600">y → −∞ (falls right)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-100 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-violet-900 mb-2">
|
||||
Memory Trick: "Even = Same Ends, Odd = Opposite Ends"
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Even degree → both ends go the same direction (both up or both
|
||||
down). Odd degree → ends go in opposite directions (one up, one
|
||||
down). The sign of the leading coefficient tells you which
|
||||
direction is "up".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-2">Example 1</p>
|
||||
<p className="font-mono text-sm text-slate-700 mb-2">
|
||||
f(x) = 3x⁴ − 2x² + 1
|
||||
</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>Degree = 4 (even)</li>
|
||||
<li>Leading coefficient = 3 (positive)</li>
|
||||
<li>Both ends rise → ↑ ↑</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-2">Example 2</p>
|
||||
<p className="font-mono text-sm text-slate-700 mb-2">
|
||||
g(x) = −2x⁵ + x³ − 4x
|
||||
</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>Degree = 5 (odd)</li>
|
||||
<li>Leading coefficient = −2 (negative)</li>
|
||||
<li>Rises left, falls right → ↑ ↓</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Degree and Number of Turns */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Degree, Turns & Intercepts
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800 mb-1">Maximum Turns</p>
|
||||
<p className="text-2xl font-mono font-bold text-violet-600">
|
||||
n − 1
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
A degree-n polynomial can change direction at most n − 1
|
||||
times.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Max x-Intercepts
|
||||
</p>
|
||||
<p className="text-2xl font-mono font-bold text-violet-600">
|
||||
n
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
A degree-n polynomial can cross the x-axis at most n times.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800 mb-1">y-Intercept</p>
|
||||
<p className="text-2xl font-mono font-bold text-violet-600">
|
||||
f(0)
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
Plug x = 0 — only the constant term survives.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PolynomialBehaviorWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Zeros & Multiplicity{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Zeros & Multiplicity */}
|
||||
<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-6">
|
||||
Zeros, Roots & Multiplicity
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The <strong>zeros</strong> (or roots) of a polynomial are the
|
||||
x-values where f(x) = 0. In factored form, each factor (x − r)
|
||||
contributes a root at x = r. The <strong>multiplicity</strong> of
|
||||
a root tells us how many times that factor is repeated, which
|
||||
determines the graph's behavior at that x-intercept.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Multiplicity Rules */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
How Multiplicity Affects the Graph
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-blue-100 text-blue-800 font-bold px-3 py-1 rounded-full text-sm">
|
||||
Odd Multiplicity (1, 3, 5…)
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-2">
|
||||
The graph <strong>crosses through</strong> the x-axis at that
|
||||
root. It passes from one side to the other.
|
||||
</p>
|
||||
<div className="bg-blue-50 rounded-lg p-3 font-mono text-sm">
|
||||
<p className="text-slate-600">f(x) = (x − 2)¹(x + 1)³</p>
|
||||
<p className="text-blue-700 font-bold mt-1">
|
||||
Both x = 2 and x = −1 are crossings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-green-100 text-green-800 font-bold px-3 py-1 rounded-full text-sm">
|
||||
Even Multiplicity (2, 4, 6…)
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-2">
|
||||
The graph <strong>touches and bounces off</strong> the x-axis
|
||||
at that root. It stays on the same side.
|
||||
</p>
|
||||
<div className="bg-green-50 rounded-lg p-3 font-mono text-sm">
|
||||
<p className="text-slate-600">f(x) = (x − 3)²(x + 2)⁴</p>
|
||||
<p className="text-green-700 font-bold mt-1">
|
||||
Both x = 3 and x = −2 are bounces
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Example */}
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Complete Analysis — Worked Example
|
||||
</p>
|
||||
<p className="font-mono text-slate-700 text-sm mb-3">
|
||||
f(x) = −2(x + 3)(x − 1)²(x − 4)
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-100 text-violet-900">
|
||||
<th className="p-2 text-left font-bold">Feature</th>
|
||||
<th className="p-2 text-left font-bold">
|
||||
Value/Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-600">
|
||||
<tr className="border-b border-violet-50">
|
||||
<td className="p-2 font-semibold">Degree</td>
|
||||
<td className="p-2">1 + 2 + 1 = 4 (even)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-violet-50 bg-violet-50">
|
||||
<td className="p-2 font-semibold">Leading coefficient</td>
|
||||
<td className="p-2">−2 (negative)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-violet-50">
|
||||
<td className="p-2 font-semibold">End behavior</td>
|
||||
<td className="p-2">
|
||||
Both ends fall (even degree, negative leading
|
||||
coefficient)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-violet-50 bg-violet-50">
|
||||
<td className="p-2 font-semibold">Zero at x = −3</td>
|
||||
<td className="p-2">
|
||||
Multiplicity 1 (odd) → graph <strong>crosses</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-violet-50">
|
||||
<td className="p-2 font-semibold">Zero at x = 1</td>
|
||||
<td className="p-2">
|
||||
Multiplicity 2 (even) → graph <strong>bounces</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-violet-50">
|
||||
<td className="p-2 font-semibold">Zero at x = 4</td>
|
||||
<td className="p-2">
|
||||
Multiplicity 1 (odd) → graph <strong>crosses</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remainder & Factor Theorems */}
|
||||
<div className="bg-violet-100 rounded-xl p-5 space-y-3">
|
||||
<h4 className="font-bold text-violet-900">
|
||||
Remainder Theorem & Factor Theorem
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="bg-white rounded-lg p-4 text-sm">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Remainder Theorem
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
When polynomial P(x) is divided by (x − a), the remainder
|
||||
equals <strong>P(a)</strong>. No long division needed — just
|
||||
plug in!
|
||||
</p>
|
||||
<div className="font-mono mt-2 bg-violet-50 rounded p-2 text-xs">
|
||||
<p>P(x) = x³ − 2x + 5, divide by (x − 2)</p>
|
||||
<p className="text-violet-700 font-bold">
|
||||
P(2) = 8 − 4 + 5 = 9 → remainder is 9
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 text-sm">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Factor Theorem
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
(x − a) is a factor of P(x) if and only if{" "}
|
||||
<strong>P(a) = 0</strong>. In other words, a is a root.
|
||||
</p>
|
||||
<div className="font-mono mt-2 bg-violet-50 rounded p-2 text-xs">
|
||||
<p>P(x) = x² − 5x + 6, check x = 2:</p>
|
||||
<p>P(2) = 4 − 10 + 6 = 0</p>
|
||||
<p className="text-violet-700 font-bold">
|
||||
So (x − 2) is a factor ✓
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
SAT Key Insight: Sum of Multiplicities = Degree
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
If you're given a graph and told the degree, you can figure out
|
||||
multiplicities. For example, a degree-4 polynomial with only 3
|
||||
x-intercepts must have one zero with multiplicity 2 (a bounce).
|
||||
Use this to match graphs to equations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MultiplicityWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[2] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{ADV_POLYNOMIAL_QUIZ.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-violet-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-violet-900 font-bold rounded-full hover:bg-violet-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolynomialFunctionsLesson;
|
||||
252
src/pages/student/lessons/ProbabilityLesson.tsx
Normal file
252
src/pages/student/lessons/ProbabilityLesson.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import React from "react";
|
||||
import { Target, Hash, GitBranch, Layers, Table, BookOpen } from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import ProbabilityTableWidget from "../../../components/lessons/ProbabilityTableWidget";
|
||||
import ProbabilityTreeWidget from "../../../components/lessons/ProbabilityTreeWidget";
|
||||
import {
|
||||
PROBABILITY_EASY,
|
||||
PROBABILITY_MEDIUM,
|
||||
} from "../../../data/math/probability";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Basic Probability", icon: Target },
|
||||
{ title: "Two-Way Tables", icon: Table },
|
||||
{ title: "Conditional Probability", icon: GitBranch },
|
||||
{ title: "Independent Events", icon: Layers },
|
||||
{ title: "Counting & Tree Diagrams", icon: Hash },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function ProbabilityLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Probability & Conditional Probability"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Basic Probability
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Probability measures how likely an event is. Always between 0
|
||||
(impossible) and 1 (certain).
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>P(A) = favorable outcomes ÷ total outcomes</FormulaBox>
|
||||
<FormulaBox>P(not A) = 1 − P(A)</FormulaBox>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>Bag: 3 red, 5 blue, 2 green marbles (10 total)</p>
|
||||
<p className="text-slate-500">
|
||||
P(blue) = 5 ÷ 10 ={" "}
|
||||
<strong className="text-amber-700">
|
||||
<Frac n="1" d="2" />
|
||||
</strong>
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
P(not blue) = 1 − <Frac n="1" d="2" /> ={" "}
|
||||
<strong className="text-amber-700">
|
||||
<Frac n="1" d="2" />
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Two-Way Tables
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Two-way tables organize data by two categories. Row and column
|
||||
totals help calculate probabilities.{" "}
|
||||
<strong>Very common on the SAT!</strong>
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2"></th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center">
|
||||
Cats
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center">
|
||||
Dogs
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-center font-bold">
|
||||
Total
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700 text-center">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold text-left">
|
||||
Male
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">50</td>
|
||||
<td className="border border-slate-200 px-3 py-2">30</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
80
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold text-left">
|
||||
Female
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">60</td>
|
||||
<td className="border border-slate-200 px-3 py-2">60</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
120
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-amber-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold text-left">
|
||||
Total
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
110
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
90
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
200
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Joint Probability" color="amber">
|
||||
<p>
|
||||
P(male AND cats) = 50 ÷ 200 ={" "}
|
||||
<strong className="text-amber-700">
|
||||
<Frac n="1" d="4" />
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ProbabilityTableWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Conditional Probability
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
P(A | B) means "probability of A <strong>given</strong> that B has
|
||||
occurred." Restrict your denominator to just the "given" group.
|
||||
</p>
|
||||
<FormulaBox>P(A | B) = P(A and B) ÷ P(B)</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: From the Table" color="amber">
|
||||
<p>P(cats | male) = ?</p>
|
||||
<p className="text-slate-500">Restrict to males only (80 total)</p>
|
||||
<p className="text-slate-500">Males who prefer cats: 50</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
P(cats | male) = 50 ÷ 80 = <Frac n="5" d="8" />
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
"Given" = restrict your denominator to just that subgroup! Don't
|
||||
use the grand total.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Independent Events
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Events are <strong>independent</strong> if one doesn't affect the
|
||||
other. For independent events:
|
||||
</p>
|
||||
<FormulaBox>P(A and B) = P(A) × P(B)</FormulaBox>
|
||||
<p className="text-slate-700 mt-3">
|
||||
Test: Events are independent if P(A | B) = P(A).
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Independent Events" color="amber">
|
||||
<p>Coin flip and die roll:</p>
|
||||
<p className="text-slate-500">
|
||||
P(heads AND 6) = <Frac n="1" d="2" /> × <Frac n="1" d="6" /> ={" "}
|
||||
<strong className="text-amber-700">
|
||||
<Frac n="1" d="12" />
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="remember">
|
||||
<p className="text-slate-700">
|
||||
"With replacement" → independent. "Without replacement" → NOT
|
||||
independent (each draw changes the remaining pool).
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Counting & Tree Diagrams
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Multiplication principle:</strong> If event A has m outcomes
|
||||
and event B has n outcomes, the total number of combined outcomes is
|
||||
m × n.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>
|
||||
3 shirts × 4 pants × 2 shoes ={" "}
|
||||
<strong className="text-amber-700">24 outfits</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ProbabilityTreeWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{PROBABILITY_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{PROBABILITY_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
480
src/pages/student/lessons/ProportionalLesson.tsx
Normal file
480
src/pages/student/lessons/ProportionalLesson.tsx
Normal file
@ -0,0 +1,480 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
Scale,
|
||||
Calculator,
|
||||
ArrowLeftRight,
|
||||
Percent,
|
||||
} from "lucide-react";
|
||||
import RatioVisualizerWidget from "../../../components/lessons/RatioVisualizerWidget";
|
||||
import UnitConversionWidget from "../../../components/lessons/UnitConversionWidget";
|
||||
import PercentChangeWidget from "../../../components/lessons/PercentChangeWidget";
|
||||
import MultiStepPercentWidget from "../../../components/lessons/MultiStepPercentWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import {
|
||||
PROPORTIONAL_QUIZ_DATA,
|
||||
PERCENTAGES_ADV_QUIZ,
|
||||
PERCENTAGES_QUIZ_DATA,
|
||||
} from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const ProportionalLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) {
|
||||
setActiveSection(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-amber-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-amber-600 text-white" : isPast ? "bg-amber-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-amber-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const allQuizzes = [
|
||||
...PROPORTIONAL_QUIZ_DATA,
|
||||
...PERCENTAGES_ADV_QUIZ,
|
||||
...PERCENTAGES_QUIZ_DATA,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Ratios" icon={Scale} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Unit Conversions"
|
||||
icon={ArrowLeftRight}
|
||||
/>
|
||||
<SectionMarker index={2} title="Percent Changes" icon={Percent} />
|
||||
<SectionMarker
|
||||
index={3}
|
||||
title="Multi-Step & Interest"
|
||||
icon={Calculator}
|
||||
/>
|
||||
<SectionMarker index={4} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Ratios */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Ratios & Proportions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A <strong>Ratio</strong> compares two quantities. A critical SAT
|
||||
skill is distinguishing between <strong>Part-to-Part</strong> and{" "}
|
||||
<strong>Part-to-Whole</strong> ratios, and knowing which one a
|
||||
question is asking for.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">Part-to-Part</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Compares one part of a whole to another part. Neither number
|
||||
is the total.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-1 rounded text-amber-700 text-sm">
|
||||
Boys : Girls = 3 : 2
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Means: for every 3 boys there are 2 girls. Total parts = 5.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">Part-to-Whole</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
Compares one part to the total. This is equivalent to a
|
||||
fraction or percentage.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-1 rounded text-amber-700 text-sm">
|
||||
Boys : Total = 3 : 5
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Means: boys make up 3 out of 5 = 60% of the total group.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 bg-slate-100 p-4 rounded-xl border-l-4 border-amber-500 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
Cross-Multiplication for Proportions
|
||||
</p>
|
||||
<p className="text-slate-700 mb-2">
|
||||
When two ratios are equal (a proportion), cross-multiply to
|
||||
solve for an unknown:
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-slate-700 font-bold mb-1">
|
||||
<Frac n="a" d="b" /> = <Frac n="c" d="d" /> → a × d = b × c
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
Example: <Frac n="3" d="4" /> = <Frac n="x" d="20" /> → 3 × 20 =
|
||||
4 × x → 60 = 4x → x = <strong>15</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
The k-Multiplier Method
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
If a ratio is a : b, the actual amounts are <strong>ak</strong>{" "}
|
||||
and <strong>bk</strong> for some positive number k. Use this
|
||||
when you know the ratio and the total.
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-1 rounded text-amber-700 text-sm mt-2">
|
||||
Ratio 3:2, Total = 25 → 3k + 2k = 25 → k = 5 → Parts: 15 and 10
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base text-slate-600 mt-4">
|
||||
Use the tool below to see how scaling a ratio (multiplying by{" "}
|
||||
<em>k</em>) keeps the proportions the same.
|
||||
</p>
|
||||
</div>
|
||||
<RatioVisualizerWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Unit Conversions{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Unit Conversions */}
|
||||
<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-6">
|
||||
Dimensional Analysis
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
To convert units, multiply by conversion fractions equal to 1
|
||||
(like <strong>5280 ft per mile</strong>). Arrange the fractions so
|
||||
the units you <em>don't</em> want cancel out (top and bottom).
|
||||
</p>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
Step-by-Step Chain Multiplication
|
||||
</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm text-slate-700">
|
||||
<li>
|
||||
Write the starting value as a fraction (e.g., 60 miles / 1
|
||||
hour).
|
||||
</li>
|
||||
<li>
|
||||
Multiply by a conversion fraction that cancels the unit you
|
||||
want to remove.
|
||||
</li>
|
||||
<li>Repeat until only the desired units remain.</li>
|
||||
<li>
|
||||
Multiply all numerators, multiply all denominators, then
|
||||
divide.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="mt-4 bg-slate-100 p-4 rounded-xl border-l-4 border-amber-500 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
Worked Example: Convert 60 miles/hour to feet/second
|
||||
</p>
|
||||
<div className="font-mono text-slate-700 space-y-2 text-xs">
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-amber-700 font-bold">Step 1:</span>{" "}
|
||||
Start with 60 miles/hr
|
||||
</div>
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-amber-700 font-bold">Step 2:</span> ×
|
||||
(5280 ft / 1 mile) ← cancels "miles"
|
||||
</div>
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-amber-700 font-bold">Step 3:</span> × (1
|
||||
hr / 3600 sec) ← cancels "hours"
|
||||
</div>
|
||||
<div className="bg-white rounded p-3 border-2 border-amber-300">
|
||||
<span className="text-amber-700 font-bold">Result:</span>{" "}
|
||||
<Frac n="60 × 5280" d="1 × 3600" /> ={" "}
|
||||
<Frac n="316,800" d="3600" /> ={" "}
|
||||
<strong className="text-amber-800">88 feet per second</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Notice how miles and hours both cancel, leaving only feet/second
|
||||
— exactly what was asked for.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<UnitConversionWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Percent Changes{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Percent Increases & Decreases */}
|
||||
<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">
|
||||
Percent Increases & Decreases
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The most important SAT skill for percentages is using{" "}
|
||||
<strong>multipliers</strong> — a single number that captures both
|
||||
the original value and the change.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
Percent Increase
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-amber-700 font-bold mb-2">
|
||||
New = Original × (1 + r)
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Where r is the rate as a decimal (e.g., 30% → r = 0.30)
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Example: $200 increased by 15% → 200 × 1.15 ={" "}
|
||||
<strong>$230</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
Percent Decrease
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-amber-700 font-bold mb-2">
|
||||
New = Original × (1 − r)
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Where r is the rate as a decimal (e.g., 25% → r = 0.25)
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Example: $200 decreased by 25% → 200 × 0.75 ={" "}
|
||||
<strong>$150</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 bg-slate-100 p-4 rounded-xl border-l-4 border-amber-500 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
Finding the Percent Change
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-slate-700 font-bold mb-1">
|
||||
% Change = <Frac n="New − Old" d="Old" /> × 100
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Positive result = increase. Negative result = decrease.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Example: Old = 80, New = 100 → <Frac n="100 − 80" d="80" /> ×
|
||||
100 = <strong>25% increase</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PercentChangeWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Multi-Step Changes{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Multi-Step Changes */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Multi-Step Percent Changes
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
<strong>Trap Alert:</strong> Percentages do not simply add up. An
|
||||
increase of 20% followed by a decrease of 20% does{" "}
|
||||
<strong>not</strong> bring you back to the start!
|
||||
</p>
|
||||
<div className="mt-4 bg-rose-50 border border-rose-200 rounded-xl p-5">
|
||||
<p className="font-bold text-rose-900 mb-2">
|
||||
Why 20% Up then 20% Down ≠ 0%
|
||||
</p>
|
||||
<div className="font-mono text-sm text-slate-700 space-y-1">
|
||||
<div>
|
||||
Start: <strong>$100</strong>
|
||||
</div>
|
||||
<div>
|
||||
+20%: 100 × 1.20 = <strong>$120</strong>
|
||||
</div>
|
||||
<div>
|
||||
−20%: 120 × 0.80 = <strong>$96</strong> ← not $100!
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 mt-2">
|
||||
The decrease applies to the <em>new</em> (larger) amount, not
|
||||
the original.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||
<p className="font-bold text-amber-900 mb-2">
|
||||
The Multiplier Chain Method
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">
|
||||
For any sequence of percent changes, multiply all the
|
||||
multipliers together:
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-amber-700 font-bold mb-2">
|
||||
Final = Original × m₁ × m₂ × m₃ × ...
|
||||
</div>
|
||||
<div className="font-mono text-sm text-slate-700 space-y-1">
|
||||
<div>
|
||||
+20% then −20%: 1.20 × 0.80 = <strong>0.96</strong> →{" "}
|
||||
<strong>4% net decrease</strong>
|
||||
</div>
|
||||
<div>
|
||||
+50% then −50%: 1.50 × 0.50 = <strong>0.75</strong> →{" "}
|
||||
<strong>25% net decrease</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 bg-slate-100 p-4 rounded-xl border-l-4 border-amber-500 text-sm">
|
||||
<p className="font-bold text-amber-900 mb-1">
|
||||
Compound Interest (Same Idea!)
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-slate-700 font-bold mb-1">
|
||||
A = P × (1 + r)ⁿ
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
P = principal, r = annual rate (decimal), n = number of years.
|
||||
This is just the multiplier chain with the same multiplier
|
||||
repeated n times.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Example: $1,000 at 5% for 3 years → 1000 × (1.05)³ ={" "}
|
||||
<strong>$1,157.63</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<MultiStepPercentWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(4)}
|
||||
className="mt-12 group flex items-center text-amber-600 font-bold hover:text-amber-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 5: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[4] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{allQuizzes.map((quiz, idx) => (
|
||||
<div key={`quiz-${idx}`} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-amber-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-amber-900 font-bold rounded-full hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProportionalLesson;
|
||||
641
src/pages/student/lessons/QuadraticEquationsLesson.tsx
Normal file
641
src/pages/student/lessons/QuadraticEquationsLesson.tsx
Normal file
@ -0,0 +1,641 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
TrendingUp,
|
||||
Grid,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import ParabolaWidget from "../../../components/lessons/ParabolaWidget";
|
||||
import DiscriminantWidget from "../../../components/lessons/DiscriminantWidget";
|
||||
import LinearQuadraticSystemWidget from "../../../components/lessons/LinearQuadraticSystemWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { QUADRATIC_EQ_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const QuadraticEquationsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-violet-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-violet-600 text-white" : isPast ? "bg-violet-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-violet-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker
|
||||
index={0}
|
||||
title="Parabolas & Forms"
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Solving & Discriminant"
|
||||
icon={RefreshCw}
|
||||
/>
|
||||
<SectionMarker index={2} title="Systems" icon={Grid} />
|
||||
<SectionMarker index={3} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Parabolas & Forms */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Parabolas & Quadratic Forms
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A quadratic function creates a U-shaped curve called a{" "}
|
||||
<strong>parabola</strong>. The SAT uses three different but
|
||||
equivalent forms — each form highlights different features.
|
||||
Knowing all three lets you pick the most efficient approach for
|
||||
each question.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Three Forms Card */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
The Three Quadratic Forms
|
||||
</h3>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="flex items-baseline gap-3 mb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-violet-500 bg-violet-100 px-2 py-0.5 rounded">
|
||||
Standard Form
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-mono text-violet-800 font-bold text-xl text-center mb-3">
|
||||
y = ax² + bx + c
|
||||
</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
<strong>a > 0</strong>: parabola opens upward (U-shape);{" "}
|
||||
<strong>a < 0</strong>: opens downward (∩-shape)
|
||||
</li>
|
||||
<li>
|
||||
<strong>|a| > 1</strong>: narrow parabola;{" "}
|
||||
<strong>|a| < 1</strong>: wide parabola
|
||||
</li>
|
||||
<li>
|
||||
<strong>c</strong> is the y-intercept (the value of y when x =
|
||||
0)
|
||||
</li>
|
||||
<li>
|
||||
Vertex x-coordinate: x = <Frac n="−b" d="2a" />
|
||||
</li>
|
||||
<li>
|
||||
Axis of symmetry: x = <Frac n="−b" d="2a" />
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-3 bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800">Example:</p>
|
||||
<p className="text-slate-600">
|
||||
y = 2x² − 8x + 6 → axis of symmetry: x ={" "}
|
||||
<Frac n="−(−8)" d="2 × 2" /> = <Frac n="8" d="4" /> ={" "}
|
||||
<strong>2</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="flex items-baseline gap-3 mb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-violet-500 bg-violet-100 px-2 py-0.5 rounded">
|
||||
Vertex Form
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-mono text-violet-800 font-bold text-xl text-center mb-3">
|
||||
y = a(x − h)² + k
|
||||
</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>Vertex is at (h, k) — read directly from the equation</li>
|
||||
<li>
|
||||
Watch the sign: y = a(x − <strong>3</strong>)² + 5 has vertex
|
||||
at (<strong>3</strong>, 5), not (−3, 5)
|
||||
</li>
|
||||
<li>
|
||||
k is the minimum value (if a > 0) or maximum value (if a
|
||||
< 0) of y
|
||||
</li>
|
||||
<li>
|
||||
Best form to use when a question asks about the vertex,
|
||||
minimum, or maximum
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-3 bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800">Example:</p>
|
||||
<p className="text-slate-600">
|
||||
y = −3(x + 2)² + 7 → vertex at (−2, 7); maximum value is{" "}
|
||||
<strong>7</strong> (since a = −3 < 0)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="flex items-baseline gap-3 mb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-violet-500 bg-violet-100 px-2 py-0.5 rounded">
|
||||
Factored Form
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-mono text-violet-800 font-bold text-xl text-center mb-3">
|
||||
y = a(x − r₁)(x − r₂)
|
||||
</p>
|
||||
<ul className="text-slate-600 text-sm space-y-1 list-disc list-inside">
|
||||
<li>x-intercepts (roots/zeros) are at x = r₁ and x = r₂</li>
|
||||
<li>Set each factor = 0 to find roots: x − r₁ = 0 → x = r₁</li>
|
||||
<li>
|
||||
Axis of symmetry is the midpoint of the roots: x ={" "}
|
||||
<Frac n="r₁ + r₂" d="2" />
|
||||
</li>
|
||||
<li>
|
||||
Best form to use when a question asks about x-intercepts or
|
||||
roots
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-3 bg-violet-50 rounded-lg p-3 text-sm">
|
||||
<p className="font-semibold text-violet-800">Example:</p>
|
||||
<p className="text-slate-600">
|
||||
y = 2(x − 1)(x − 5) → roots at x = 1 and x = 5; axis of
|
||||
symmetry: x = <Frac n="1 + 5" d="2" /> = <strong>3</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Converting Between Forms */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Converting Between Forms
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-2">
|
||||
Standard → Vertex (Completing the Square)
|
||||
</p>
|
||||
<div className="text-sm text-slate-600 space-y-1 font-mono">
|
||||
<p>y = x² − 6x + 5</p>
|
||||
<p>= (x² − 6x + 9) − 9 + 5</p>
|
||||
<p>= (x − 3)² − 4</p>
|
||||
<p className="text-violet-700 font-bold">Vertex: (3, −4)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-2">
|
||||
Standard → Factored
|
||||
</p>
|
||||
<div className="text-sm text-slate-600 space-y-1 font-mono">
|
||||
<p>y = x² − 6x + 5</p>
|
||||
<p>Find two numbers: × = 5, + = −6</p>
|
||||
<p>Those numbers: −1 and −5</p>
|
||||
<p>= (x − 1)(x − 5)</p>
|
||||
<p className="text-violet-700 font-bold">
|
||||
Roots: x = 1 and x = 5
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
Common SAT Trap — Vertex Form Sign
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
y = (x + 4)² − 3 looks like h = 4, but it's actually y = (x −
|
||||
(−4))² − 3, so the vertex is at (<strong>−4</strong>, −3).
|
||||
Always rewrite (x + h) as (x − (−h)) to read the vertex
|
||||
correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParabolaWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Solving & Discriminant{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Solving & Discriminant */}
|
||||
<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-6">
|
||||
Solving Quadratics & The Discriminant
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
There are four methods to solve a quadratic equation. Choosing the
|
||||
right method quickly is an important SAT skill. Before diving in,
|
||||
check the <strong>Discriminant</strong> — it tells you instantly
|
||||
how many real solutions exist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Discriminant Card */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-6 space-y-4">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
The Discriminant
|
||||
</h3>
|
||||
<div className="bg-white rounded-xl p-4 text-center border border-violet-100">
|
||||
<p className="text-2xl font-mono font-bold text-violet-800">
|
||||
Δ = b² − 4ac
|
||||
</p>
|
||||
<p className="text-slate-500 text-sm mt-1">
|
||||
For ax² + bx + c = 0
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-200 text-violet-900">
|
||||
<th className="p-3 rounded-tl-lg font-bold text-left">
|
||||
Discriminant Value
|
||||
</th>
|
||||
<th className="p-3 font-bold text-left">
|
||||
Number of Real Solutions
|
||||
</th>
|
||||
<th className="p-3 rounded-tr-lg font-bold text-left">
|
||||
What it Means Graphically
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="bg-white border-b border-violet-100">
|
||||
<td className="p-3 font-bold text-green-700">Δ > 0</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
<strong>2</strong> distinct real solutions
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Parabola crosses x-axis at two points
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-violet-50 border-b border-violet-100">
|
||||
<td className="p-3 font-bold text-amber-700">Δ = 0</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
<strong>1</strong> repeated real solution
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Parabola is tangent to x-axis (vertex on x-axis)
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-bold text-red-700">Δ < 0</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
<strong>0</strong> real solutions
|
||||
</td>
|
||||
<td className="p-3 text-slate-600">
|
||||
Parabola does not touch x-axis
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Four Solving Methods */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Four Methods to Solve ax² + bx + c = 0
|
||||
</h3>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Method 1: Factoring (fastest when it works)
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
Find two numbers that multiply to <strong>a × c</strong> and add
|
||||
to <strong>b</strong>.
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 font-mono text-sm space-y-1">
|
||||
<p className="text-slate-600">Solve: x² + 5x + 6 = 0</p>
|
||||
<p className="text-slate-600">
|
||||
Need two numbers: × = 6, + = 5 → <strong>2 and 3</strong>
|
||||
</p>
|
||||
<p className="text-slate-600">(x + 2)(x + 3) = 0</p>
|
||||
<p className="text-violet-700 font-bold">x = −2 or x = −3</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Method 2: Square Root Method (when b = 0 or vertex form)
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
Isolate the squared term, then take the square root of both
|
||||
sides. Remember ±.
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 font-mono text-sm space-y-1">
|
||||
<p className="text-slate-600">Solve: 2x² − 18 = 0</p>
|
||||
<p className="text-slate-600">2x² = 18 → x² = 9</p>
|
||||
<p className="text-slate-600">x = ±√9</p>
|
||||
<p className="text-violet-700 font-bold">x = 3 or x = −3</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Method 3: Quadratic Formula (always works)
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-center mb-3">
|
||||
<p className="font-mono text-violet-800 font-bold text-lg">
|
||||
x = <Frac n={<>−b ± √(b² − 4ac)</>} d="2a" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 rounded-lg p-3 font-mono text-sm space-y-1">
|
||||
<p className="text-slate-600">
|
||||
Solve: 2x² − 3x − 2 = 0 (a=2, b=−3, c=−2)
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
Δ = (−3)² − 4(2)(−2) = 9 + 16 = 25
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
x = <Frac n="3 ± √25" d="4" /> = <Frac n="3 ± 5" d="4" />
|
||||
</p>
|
||||
<p className="text-violet-700 font-bold">x = 2 or x = −½</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Method 4: Completing the Square
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
Rewrite into vertex form, then solve. Most useful when the SAT
|
||||
asks for vertex form or when coefficients are simple.
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 font-mono text-sm space-y-1">
|
||||
<p className="text-slate-600">Solve: x² + 6x + 5 = 0</p>
|
||||
<p className="text-slate-600">x² + 6x = −5</p>
|
||||
<p className="text-slate-600">
|
||||
x² + 6x + 9 = −5 + 9 (add (<Frac n="6" d="2" />
|
||||
)² = 9 to both sides)
|
||||
</p>
|
||||
<p className="text-slate-600">(x + 3)² = 4</p>
|
||||
<p className="text-slate-600">x + 3 = ±2</p>
|
||||
<p className="text-violet-700 font-bold">x = −1 or x = −5</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SAT Strategy */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-amber-900 mb-3">
|
||||
SAT Strategy: Which Method to Use?
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-slate-700">
|
||||
<div className="flex gap-3 items-start">
|
||||
<span className="font-bold text-violet-700 shrink-0">1.</span>
|
||||
<p>
|
||||
If the equation factors nicely (integer roots likely) →{" "}
|
||||
<strong>Factor</strong> first. It's fastest.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 items-start">
|
||||
<span className="font-bold text-violet-700 shrink-0">2.</span>
|
||||
<p>
|
||||
If the middle term is missing (bx = 0) or it's already in
|
||||
vertex form → <strong>Square Root Method</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 items-start">
|
||||
<span className="font-bold text-violet-700 shrink-0">3.</span>
|
||||
<p>
|
||||
If you can't factor quickly or need exact answers →{" "}
|
||||
<strong>Quadratic Formula</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 items-start">
|
||||
<span className="font-bold text-violet-700 shrink-0">4.</span>
|
||||
<p>
|
||||
If the question asks to "rewrite in vertex form" or "find the
|
||||
vertex" → <strong>Complete the Square</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DiscriminantWidget />
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Linear-Quadratic Systems{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Systems */}
|
||||
<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">
|
||||
Linear-Quadratic Systems
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
When a line intersects a parabola, the system can have 0, 1, or 2
|
||||
solutions. The key strategy is to substitute the linear equation
|
||||
into the quadratic, rearrange everything to one side to form a new
|
||||
quadratic, then use the discriminant to determine the number of
|
||||
intersections.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Strategy: 4-Step Process
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
step: "1",
|
||||
title: "Write out the system",
|
||||
body: "You have a linear equation (y = mx + b) and a quadratic (y = ax² + bx + c). Make sure both are in y = form.",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
title: "Set the right sides equal",
|
||||
body: "Since both equal y, set them equal to each other: mx + b = ax² + bx + c.",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
title: "Rearrange to zero",
|
||||
body: "Move everything to one side: 0 = ax² + (b−m)x + (c−b). You now have a new quadratic equation.",
|
||||
},
|
||||
{
|
||||
step: "4",
|
||||
title: "Use the Discriminant on the new quadratic",
|
||||
body: "Δ > 0: line crosses parabola at 2 points. Δ = 0: line is tangent (1 point). Δ < 0: line misses parabola (0 points).",
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.step}
|
||||
className="flex gap-4 bg-white rounded-xl p-4 border border-violet-100"
|
||||
>
|
||||
<div className="w-8 h-8 bg-violet-600 text-white rounded-full flex items-center justify-center font-bold shrink-0">
|
||||
{item.step}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 mb-1">
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm">{item.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-100 rounded-xl p-5">
|
||||
<p className="font-bold text-violet-900 mb-2">Worked Example</p>
|
||||
<p className="text-sm text-slate-700 mb-3">
|
||||
Find all intersections of y = 2x + 1 and y = x² − 2x + 3.
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-600">
|
||||
<p>Set equal: 2x + 1 = x² − 2x + 3</p>
|
||||
<p>Rearrange: 0 = x² − 4x + 2</p>
|
||||
<p>Discriminant: Δ = (−4)² − 4(1)(2) = 16 − 8 = 8</p>
|
||||
<p className="text-violet-700 font-bold">
|
||||
Δ > 0 → 2 intersection points
|
||||
</p>
|
||||
<p>
|
||||
x = <Frac n="4 ± √8" d="2" /> = 2 ± √2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
Key Distinction: Tangent vs. Intersects
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
When the SAT says the line is <em>tangent</em> to the parabola,
|
||||
that means exactly 1 intersection → set Δ = 0 and solve for the
|
||||
unknown constant.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinearQuadraticSystemWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{QUADRATIC_EQ_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-violet-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-violet-900 font-bold rounded-full hover:bg-violet-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuadraticEquationsLesson;
|
||||
473
src/pages/student/lessons/RationalRadicalLesson.tsx
Normal file
473
src/pages/student/lessons/RationalRadicalLesson.tsx
Normal file
@ -0,0 +1,473 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Target, Grid } from "lucide-react";
|
||||
import RationalExplorer from "../../../components/lessons/RationalExplorer";
|
||||
import RadicalSolutionWidget from "../../../components/lessons/RadicalSolutionWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { ADV_RATIONAL_QUIZ } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const RationalRadicalLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-violet-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-violet-600 text-white" : isPast ? "bg-violet-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-violet-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Rational Functions" icon={Grid} />
|
||||
<SectionMarker index={1} title="Radical Equations" icon={Target} />
|
||||
<SectionMarker index={2} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Rational Functions */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Rational Functions & Discontinuities
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A <strong>rational function</strong> is any function of the form
|
||||
f(x) = P(x) ÷ Q(x), where both P and Q are polynomials. Wherever
|
||||
Q(x) = 0, the function is undefined — creating a{" "}
|
||||
<strong>discontinuity</strong>. There are two types of
|
||||
discontinuities: holes and vertical asymptotes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Holes vs Vertical Asymptotes */}
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Holes vs. Vertical Asymptotes
|
||||
</h3>
|
||||
|
||||
<div className="bg-violet-100 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-violet-900 mb-1">
|
||||
Key Step: Always Factor Both Numerator and Denominator First
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Once factored, look at the denominator's zeros. If a zero also
|
||||
cancels with the numerator → <strong>hole</strong>. If it does
|
||||
not cancel → <strong>vertical asymptote</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-amber-100 text-amber-800 font-bold px-3 py-1 rounded-full text-sm">
|
||||
Hole (Removable Discontinuity)
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-2">
|
||||
Occurs when a factor in the denominator{" "}
|
||||
<strong>also cancels</strong> with a factor in the numerator.
|
||||
The function is undefined at that x-value, but there's no
|
||||
asymptote.
|
||||
</p>
|
||||
<div className="bg-amber-50 rounded-lg p-3 font-mono text-sm">
|
||||
<p className="text-slate-600">
|
||||
f(x) = <Frac n="(x − 2)(x + 3)" d="(x − 2)(x − 1)" />
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
After canceling: f(x) = <Frac n="x + 3" d="x − 1" />
|
||||
</p>
|
||||
<p className="text-amber-700 font-bold">
|
||||
Hole at x = 2 (canceled factor)
|
||||
</p>
|
||||
<p className="text-violet-700 font-bold">
|
||||
V. asymptote at x = 1 (remains)
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs mt-2">
|
||||
To find the y-coordinate of the hole: plug x = 2 into the
|
||||
simplified function: f(2) = (2 + 3) ÷ (2 − 1) = 5. Hole is at
|
||||
(2, 5).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-red-100 text-red-800 font-bold px-3 py-1 rounded-full text-sm">
|
||||
Vertical Asymptote
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-2">
|
||||
Occurs when a denominator factor does{" "}
|
||||
<strong>NOT cancel</strong>. The function approaches ±∞ near
|
||||
that x-value. The graph has a vertical line it never crosses.
|
||||
</p>
|
||||
<div className="bg-red-50 rounded-lg p-3 font-mono text-sm">
|
||||
<p className="text-slate-600">
|
||||
f(x) = <Frac n="x + 1" d="(x − 3)(x + 2)" />
|
||||
</p>
|
||||
<p className="text-slate-600">No factors cancel.</p>
|
||||
<p className="text-red-700 font-bold">
|
||||
V. asymptotes at x = 3 and x = −2
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs mt-2">
|
||||
The denominator has zeros at x = 3 and x = −2. Neither cancels
|
||||
with the numerator, so both are vertical asymptotes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizontal Asymptotes */}
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Horizontal Asymptotes — End Behavior of Rational Functions
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
Compare the degree of the numerator (n) to the degree of the
|
||||
denominator (d):
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-violet-100 text-violet-900">
|
||||
<th className="p-2 text-left font-bold">Condition</th>
|
||||
<th className="p-2 text-left font-bold">
|
||||
Horizontal Asymptote
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-600">
|
||||
<tr className="border-b border-violet-50">
|
||||
<td className="p-2 font-semibold">
|
||||
n < d (numerator degree lower)
|
||||
</td>
|
||||
<td className="p-2">y = 0</td>
|
||||
</tr>
|
||||
<tr className="border-b border-violet-50 bg-violet-50">
|
||||
<td className="p-2 font-semibold">n = d (same degree)</td>
|
||||
<td className="p-2">
|
||||
y ={" "}
|
||||
<Frac
|
||||
n="leading coefficient of top"
|
||||
d="leading coefficient of bottom"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2 font-semibold">
|
||||
n > d (numerator degree higher)
|
||||
</td>
|
||||
<td className="p-2">
|
||||
No horizontal asymptote (oblique asymptote instead)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-3 bg-violet-50 rounded-lg p-3 text-sm font-mono">
|
||||
<p className="text-slate-600">
|
||||
f(x) = <Frac n="3x²" d="x² − 4" />: n = d = 2 → H.A. at y ={" "}
|
||||
<Frac n="3" d="1" /> ={" "}
|
||||
<strong className="text-violet-700">3</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RationalExplorer />
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Radical Equations{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Radical Equations */}
|
||||
<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-6">
|
||||
Radical Equations & Extraneous Solutions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A <strong>radical equation</strong> has the variable under a
|
||||
square root (or other radical). The key strategy is to isolate the
|
||||
radical and then raise both sides to the appropriate power to
|
||||
eliminate it. This process can introduce{" "}
|
||||
<strong>extraneous solutions</strong> — values that satisfy the
|
||||
transformed equation but not the original. You{" "}
|
||||
<strong>must always check</strong> your answers in the original
|
||||
equation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-violet-900">
|
||||
Solving Radical Equations: Step-by-Step
|
||||
</h3>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">General Process</p>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{
|
||||
step: "1",
|
||||
text: "Isolate the radical on one side of the equation.",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
text: "Square both sides (for square roots) or cube both sides (for cube roots).",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
text: "Solve the resulting equation (may be linear or quadratic).",
|
||||
},
|
||||
{
|
||||
step: "4",
|
||||
text: "CHECK all solutions in the ORIGINAL equation. Reject any that make the original false.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.step} className="flex gap-3 items-start">
|
||||
<div className="w-6 h-6 bg-violet-600 text-white rounded-full flex items-center justify-center font-bold text-xs shrink-0">
|
||||
{item.step}
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm">{item.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-green-700 mb-3">
|
||||
Example 1 — No Extraneous Solution
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-600">
|
||||
<p>√(x + 3) = 5</p>
|
||||
<p>Square both sides:</p>
|
||||
<p>x + 3 = 25</p>
|
||||
<p>x = 22</p>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Check: √(22 + 3) = √25 = 5 ✓
|
||||
</p>
|
||||
<p className="text-green-700 font-bold">x = 22 ✓ (valid)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-red-700 mb-3">
|
||||
Example 2 — Extraneous Solution Found!
|
||||
</p>
|
||||
<div className="font-mono text-sm space-y-1 text-slate-600">
|
||||
<p>√(2x + 1) = x − 1</p>
|
||||
<p>Square both sides:</p>
|
||||
<p>2x + 1 = x² − 2x + 1</p>
|
||||
<p>0 = x² − 4x → x(x − 4) = 0</p>
|
||||
<p>x = 0 or x = 4</p>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Check x = 0: √1 = 0 − 1 → 1 ≠ −1 ✗
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Check x = 4: √9 = 4 − 1 → 3 = 3 ✓
|
||||
</p>
|
||||
<p className="text-red-700 font-bold">
|
||||
x = 0 is extraneous! Only x = 4.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Why Extraneous Solutions Occur */}
|
||||
<div className="bg-violet-100 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-violet-900 mb-1">
|
||||
Why Do Extraneous Solutions Appear?
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
Squaring both sides is not a reversible step — if you square a
|
||||
negative number, it becomes positive. For example, if the
|
||||
original equation has √x = A, then A must be non-negative (a
|
||||
square root can never give a negative output). But squaring
|
||||
produces solutions for both A and −A. Any solution that would
|
||||
require the radical to equal a negative number is extraneous.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Rational Exponents */}
|
||||
<div className="bg-white rounded-xl p-5 border border-violet-100">
|
||||
<p className="font-bold text-violet-800 mb-3">
|
||||
Rational Exponents: Connecting Radicals and Powers
|
||||
</p>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-center mb-3">
|
||||
<p className="font-mono text-violet-800 font-bold text-lg">
|
||||
x<sup>m/n</sup> = (<sup>n</sup>√x)<sup>m</sup> = <sup>n</sup>
|
||||
√(x<sup>m</sup>)
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-center">
|
||||
<p className="font-mono text-violet-700 font-bold">
|
||||
x<sup>1/2</sup> = √x
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
Exponent ½ = square root
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-center">
|
||||
<p className="font-mono text-violet-700 font-bold">
|
||||
x<sup>1/3</sup> = <sup>3</sup>√x
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
Exponent ⅓ = cube root
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 rounded-lg p-3 text-center">
|
||||
<p className="font-mono text-violet-700 font-bold">
|
||||
x<sup>2/3</sup> = (<sup>3</sup>√x)²
|
||||
</p>
|
||||
<p className="text-slate-500 text-xs mt-1">
|
||||
Numerator = power, denominator = root
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 bg-violet-50 rounded-lg p-3 font-mono text-sm">
|
||||
<p className="text-slate-600">
|
||||
Solve: x<sup>2/3</sup> = 4
|
||||
</p>
|
||||
<p className="text-slate-600">
|
||||
Raise both sides to power 3/2: x = 4<sup>3/2</sup> = (√4)³ =
|
||||
2³ = <strong className="text-violet-700">8</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
SAT Trap: The Domain of Radical Functions
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
For even roots (square root, fourth root, etc.), the expression
|
||||
under the radical must be <strong>≥ 0</strong>. The SAT may test
|
||||
this by asking for the domain of f(x) = √(2x − 6). Set 2x − 6 ≥
|
||||
0 → x ≥ 3. The domain is x ≥ 3.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadicalSolutionWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-violet-600 font-bold hover:text-violet-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[2] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{ADV_RATIONAL_QUIZ.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-violet-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-violet-900 font-bold rounded-full hover:bg-violet-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RationalRadicalLesson;
|
||||
55
src/pages/student/lessons/RatiosLesson.tsx
Normal file
55
src/pages/student/lessons/RatiosLesson.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import UnitConversionWidget from "../../../components/lessons/UnitConversionWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { RATIOS_QUIZ_DATA } from "../../../utils/constants";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const RatiosLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<div className="flex-1 max-w-4xl mx-auto p-6 md:p-12 w-full">
|
||||
<section className="mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Unit Conversions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The secret to unit conversions is{" "}
|
||||
<strong>Dimensional Analysis</strong>. Multiply by fractions equal
|
||||
to 1 (like 5280 ft / 1 mile) such that the units you don't want
|
||||
cancel out.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UnitConversionWidget />
|
||||
</section>
|
||||
|
||||
<section className="mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{RATIOS_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<div className="flex justify-center mb-12">
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="flex items-center gap-2 px-8 py-4 bg-amber-600 text-white rounded-full font-bold hover:bg-amber-700 transition-colors shadow-lg"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" /> Back to Course List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RatiosLesson;
|
||||
233
src/pages/student/lessons/RatiosRatesLesson.tsx
Normal file
233
src/pages/student/lessons/RatiosRatesLesson.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Scale,
|
||||
ArrowRight,
|
||||
Hash,
|
||||
Repeat,
|
||||
Layers,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import RatioVisualizerWidget from "../../../components/lessons/RatioVisualizerWidget";
|
||||
import UnitConversionWidget from "../../../components/lessons/UnitConversionWidget";
|
||||
import {
|
||||
RATIOS_EASY,
|
||||
RATIOS_MEDIUM,
|
||||
} from "../../../data/math/ratios-rates-proportions";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Ratios & Proportions", icon: Scale },
|
||||
{ title: "Unit Rates", icon: ArrowRight },
|
||||
{ title: "Setting Up Proportions", icon: Hash },
|
||||
{ title: "Unit Conversions", icon: Repeat },
|
||||
{ title: "Direct & Inverse Variation", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function RatiosRatesLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Ratios, Rates & Proportional Relationships"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Ratios & Proportions
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>ratio</strong> compares two quantities. A{" "}
|
||||
<strong>proportion</strong> states that two ratios are equal.
|
||||
Cross-multiplication is the key solving technique.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
<Frac n="a" d="b" /> = <Frac n="c" d="d" /> means a × d
|
||||
= b × c
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Part-to-Whole Ratio" color="amber">
|
||||
<p>Boys to girls ratio is 3 : 5. Total students: 40.</p>
|
||||
<p className="text-slate-500">
|
||||
Boys = <Frac n="3" d="8" /> × 40 = 15
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Girls = <Frac n="5" d="8" /> × 40 = 25
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<RatioVisualizerWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Unit Rates
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>unit rate</strong> has a denominator of 1 — cost per item,
|
||||
miles per hour, etc. Divide to find the unit rate.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Speed" color="amber">
|
||||
<p>360 miles in 6 hours</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
360 ÷ 6 = 60 miles per hour
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Unit Price" color="amber">
|
||||
<p>$45 for 12 items</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
$45 ÷ 12 = $3.75 per item
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
On a graph, the unit rate equals the slope. In y = mx, the slope m
|
||||
is the unit rate (y per x).
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Setting Up Proportions
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The key is matching units: ensure numerators represent the same
|
||||
quantity and denominators represent the same quantity.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Recipe Scaling" color="amber">
|
||||
<p>3 cups flour for 24 cookies. How much for 60 cookies?</p>
|
||||
<p className="text-slate-500">
|
||||
<Frac n="3" d="24" /> = <Frac n="x" d="60" />
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
24x = 180 → <strong className="text-amber-700">x = 7.5 cups</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Proportion problems on the SAT often involve scale factors, maps,
|
||||
or similar figures. Always label your units to avoid mix-ups.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Unit Conversions
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Multiply by conversion factors written as fractions equal to 1.
|
||||
Cancel units like you cancel numbers.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Distance" color="amber">
|
||||
<p>Convert 5 km to meters:</p>
|
||||
<p className="text-slate-500">
|
||||
5 km × (1000 m ÷ 1 km) ={" "}
|
||||
<strong className="text-amber-700">5000 m</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Speed" color="amber">
|
||||
<p>Convert 90 km/h to m/s:</p>
|
||||
<p className="text-slate-500">
|
||||
90 × (1000 ÷ 3600) ={" "}
|
||||
<strong className="text-amber-700">25 m/s</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<UnitConversionWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Direct & Inverse Variation
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
<strong>Direct variation:</strong> y = kx (as x increases, y
|
||||
increases proportionally). <strong>Inverse variation:</strong> xy =
|
||||
k (as x increases, y decreases).
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>Direct: y = kx → k = y ÷ x (constant ratio)</FormulaBox>
|
||||
<FormulaBox>
|
||||
Inverse: xy = k → y = k ÷ x (constant product)
|
||||
</FormulaBox>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Direct" color="amber">
|
||||
<p>
|
||||
y varies directly with x. If y = 12 when x = 4, find y when x = 7.
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
k = 12 ÷ 4 = 3 → y = 3(7) ={" "}
|
||||
<strong className="text-amber-700">21</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Inverse" color="amber">
|
||||
<p>
|
||||
y varies inversely with x. If y = 6 when x = 8, find y when x =
|
||||
12.
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
k = 6 × 8 = 48 → y = 48 ÷ 12 ={" "}
|
||||
<strong className="text-amber-700">4</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{RATIOS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{RATIOS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
298
src/pages/student/lessons/RightTrianglesTrigLesson.tsx
Normal file
298
src/pages/student/lessons/RightTrianglesTrigLesson.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Triangle,
|
||||
Ruler,
|
||||
Target,
|
||||
Hash,
|
||||
Layers,
|
||||
Circle,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import UnitCircleWidget from "../../../components/lessons/UnitCircleWidget";
|
||||
import {
|
||||
RIGHT_TRI_TRIG_EASY,
|
||||
RIGHT_TRI_TRIG_MEDIUM,
|
||||
} from "../../../data/math/right-triangles-trig";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "SOH-CAH-TOA", icon: Triangle },
|
||||
{ title: "Finding Sides", icon: Ruler },
|
||||
{ title: "Finding Angles", icon: Target },
|
||||
{ title: "Special Right Triangles", icon: Hash },
|
||||
{ title: "Complementary Identity", icon: Layers },
|
||||
{ title: "Unit Circle Basics", icon: Circle },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function RightTrianglesTrigLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Right Triangles & Trigonometry"
|
||||
sections={SECTIONS}
|
||||
color="emerald"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: SOH-CAH-TOA */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
SOH-CAH-TOA
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The three trigonometric ratios relate the sides of a right triangle
|
||||
to its acute angles:
|
||||
</p>
|
||||
<div className="space-y-3 mt-4">
|
||||
<FormulaBox>
|
||||
<strong>S</strong>in θ = <strong>O</strong>pposite ÷{" "}
|
||||
<strong>H</strong>ypotenuse
|
||||
</FormulaBox>
|
||||
<FormulaBox>
|
||||
<strong>C</strong>os θ = <strong>A</strong>djacent ÷{" "}
|
||||
<strong>H</strong>ypotenuse
|
||||
</FormulaBox>
|
||||
<FormulaBox>
|
||||
<strong>T</strong>an θ = <strong>O</strong>pposite ÷{" "}
|
||||
<strong>A</strong>djacent
|
||||
</FormulaBox>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
"Opposite" and "adjacent" depend on which angle you're looking at!
|
||||
Always identify your reference angle first.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Finding Sides */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Finding Sides
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
When you know one side and one acute angle, use trig to find the
|
||||
other sides.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Using Sine" color="emerald">
|
||||
<p>Angle = 35°, hypotenuse = 12. Find the opposite side.</p>
|
||||
<p className="text-slate-500">sin 35° = x ÷ 12</p>
|
||||
<p className="text-slate-500">
|
||||
x = 12 × sin 35° ≈{" "}
|
||||
<strong className="text-emerald-700">6.88</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Using Tangent" color="emerald">
|
||||
<p>Angle = 50°, adjacent = 8. Find the opposite side.</p>
|
||||
<p className="text-slate-500">tan 50° = x ÷ 8</p>
|
||||
<p className="text-slate-500">
|
||||
x = 8 × tan 50° ≈{" "}
|
||||
<strong className="text-emerald-700">9.53</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Finding Angles */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Finding Angles
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Use <strong>inverse trig functions</strong> (sin⁻¹, cos⁻¹, tan⁻¹)
|
||||
when you know two sides and want to find an angle.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="emerald">
|
||||
<p>Opposite = 5, hypotenuse = 13</p>
|
||||
<p className="text-slate-500">sin θ = 5 ÷ 13</p>
|
||||
<p className="text-slate-500">
|
||||
θ = sin⁻¹(5 ÷ 13) ≈{" "}
|
||||
<strong className="text-emerald-700">22.6°</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Recognizing 45°" color="emerald">
|
||||
<p>Opposite = 7, adjacent = 7</p>
|
||||
<p className="text-slate-500">tan θ = 7 ÷ 7 = 1</p>
|
||||
<p className="text-slate-500">
|
||||
θ = tan⁻¹(1) = <strong className="text-emerald-700">45°</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Special Right Triangles */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Exact Trig Values
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Know these exact values from special right triangles — they come up
|
||||
constantly!
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-emerald-100 text-emerald-900">
|
||||
<th className="border border-emerald-300 px-3 py-2 font-bold">
|
||||
Angle
|
||||
</th>
|
||||
<th className="border border-emerald-300 px-3 py-2 font-bold">
|
||||
sin
|
||||
</th>
|
||||
<th className="border border-emerald-300 px-3 py-2 font-bold">
|
||||
cos
|
||||
</th>
|
||||
<th className="border border-emerald-300 px-3 py-2 font-bold">
|
||||
tan
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700 text-center font-mono">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
30°
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="1" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="√3" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="√3" d="3" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
45°
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="√2" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="√2" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">1</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-bold">
|
||||
60°
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="√3" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
<Frac n="1" d="2" />
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">√3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
If you see √2 ÷ 2 or √3 ÷ 2 in answer choices, it's almost certainly
|
||||
a special triangle problem!
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Complementary Identity */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Complementary Angle Identity
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
In a right triangle, the two acute angles are complementary (sum to
|
||||
90°). The sine of one equals the cosine of the other.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
sin(x°) = cos(90° − x°) and cos(x°) = sin(90° − x°)
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Examples" color="emerald">
|
||||
<p>sin 25° = cos 65°</p>
|
||||
<p className="text-slate-500">cos 40° = sin 50°</p>
|
||||
<p className="text-slate-500">sin 72° = cos 18°</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
The SAT loves: "sin(a°) = cos(b°), what is a + b?" The answer is{" "}
|
||||
<strong>always 90</strong>.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6: Unit Circle */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Unit Circle Basics
|
||||
</h2>
|
||||
<ConceptCard color="emerald">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The unit circle has radius 1, centered at the origin. Any point on
|
||||
the circle is (cos θ, sin θ). This extends trig beyond right
|
||||
triangles to any angle.
|
||||
</p>
|
||||
<div className="mt-4 bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
||||
<p className="font-bold text-emerald-900 text-sm mb-2">
|
||||
Radians ↔ Degrees
|
||||
</p>
|
||||
<FormulaBox>180° = π radians</FormulaBox>
|
||||
<p className="text-sm text-slate-700 mt-2">
|
||||
To convert: degrees × (π ÷ 180) = radians
|
||||
</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<UnitCircleWidget />
|
||||
</div>
|
||||
<ExampleCard title="Example: Convert" color="emerald">
|
||||
<p>
|
||||
Convert 60° to radians: 60 × (π ÷ 180) ={" "}
|
||||
<strong className="text-emerald-700">π ÷ 3</strong>
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Convert π÷4 to degrees: (π÷4) × (180÷π) ={" "}
|
||||
<strong className="text-emerald-700">45°</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 7: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{RIGHT_TRI_TRIG_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
{RIGHT_TRI_TRIG_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="emerald" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
297
src/pages/student/lessons/SampleStatsLesson.tsx
Normal file
297
src/pages/student/lessons/SampleStatsLesson.tsx
Normal file
@ -0,0 +1,297 @@
|
||||
import React from "react";
|
||||
import { Scale, Target, BarChart, Layers, Hash, BookOpen } from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import SamplingVisualizerWidget from "../../../components/lessons/SamplingVisualizerWidget";
|
||||
import ConfidenceIntervalWidget from "../../../components/lessons/ConfidenceIntervalWidget";
|
||||
import {
|
||||
SAMPLE_STATS_EASY,
|
||||
SAMPLE_STATS_MEDIUM,
|
||||
} from "../../../data/math/sample-statistics-moe";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Random Sampling", icon: Scale },
|
||||
{ title: "Sampling Methods", icon: Layers },
|
||||
{ title: "Confidence Intervals", icon: Target },
|
||||
{ title: "Margin of Error", icon: BarChart },
|
||||
{ title: "Sample Size Effects", icon: Hash },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function SampleStatsLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Inference from Sample Statistics & Margin of Error"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Random Sampling
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
To generalize from a sample to a population, the sample must be{" "}
|
||||
<strong>random and representative</strong>. Convenience samples
|
||||
introduce bias and cannot support valid generalizations.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 mb-1">Population</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
The entire group you want to study
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 mb-1">Sample</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
The subset actually studied
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<SamplingVisualizerWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Sampling Methods
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Method
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Description
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 text-left font-bold">
|
||||
Bias Risk
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Simple Random
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Every individual has equal chance
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-700 font-semibold">
|
||||
Very Low
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Stratified
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Divide into subgroups, sample each
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-700 font-semibold">
|
||||
Very Low
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Cluster
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Randomly select entire subgroups
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-700 font-semibold">
|
||||
Low–Med
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Systematic
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Every kth individual from a list
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-700 font-semibold">
|
||||
Low–Med
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold">
|
||||
Convenience
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
Whoever is easiest to reach
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-rose-700 font-semibold">
|
||||
High
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
The SAT tests whether you can identify bias. A voluntary response
|
||||
survey or convenience sample <strong>cannot</strong> generalize to
|
||||
the whole population.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Confidence Intervals
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>confidence interval</strong> gives a range where the true
|
||||
population parameter likely falls.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
Confidence Interval = Sample Statistic ± Margin of Error
|
||||
</FormulaBox>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>Survey: 62% support a policy, margin of error ±4%</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
True support likely between 58% and 66%
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ConfidenceIntervalWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Intervals Don't Overlap
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Significant difference between groups
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
|
||||
<p className="font-bold text-amber-800 text-sm">
|
||||
Intervals Overlap
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Cannot claim a significant difference
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Margin of Error
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The margin of error tells you how much the sample statistic might
|
||||
differ from the population parameter. It depends on sample size and
|
||||
confidence level.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100 text-sm">
|
||||
<p className="font-bold text-amber-800 mb-1">Larger Sample</p>
|
||||
<p className="text-slate-600">
|
||||
→ Smaller margin of error (more precise)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-amber-100 text-sm">
|
||||
<p className="font-bold text-amber-800 mb-1">Higher Confidence</p>
|
||||
<p className="text-slate-600">
|
||||
→ Larger margin of error (wider interval)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Sample Size Matters" color="amber">
|
||||
<p>Poll of 500 people: 45% ± 3%</p>
|
||||
<p className="text-slate-500">Poll of 2000 people: 45% ± 1.5%</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Larger sample = more precise estimate
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Sample Size Effects
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Increasing sample size decreases variability and margin of error,
|
||||
but <strong>does NOT fix bias</strong>. A biased sample stays biased
|
||||
no matter how large!
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-emerald-700 text-sm">Large Random</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Best: generalizable + precise
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700 text-sm">Small Random</p>
|
||||
<p className="text-xs text-slate-500">OK but more variability</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-rose-700 text-sm">Large Biased</p>
|
||||
<p className="text-xs text-slate-500">Still biased!</p>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-red-700 text-sm">Small Biased</p>
|
||||
<p className="text-xs text-slate-500">Worst of both worlds</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
The SAT loves asking "what is wrong with this study?" Look for:
|
||||
non-random sample, voluntary response, leading questions, or
|
||||
too-small sample size.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{SAMPLE_STATS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{SAMPLE_STATS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
196
src/pages/student/lessons/SystemsEq2VarLesson.tsx
Normal file
196
src/pages/student/lessons/SystemsEq2VarLesson.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Target,
|
||||
ArrowRight,
|
||||
BarChart,
|
||||
Lightbulb,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import LinearQuadraticSystemWidget from "../../../components/lessons/LinearQuadraticSystemWidget";
|
||||
import {
|
||||
NONLINEAR_EQ_EASY,
|
||||
NONLINEAR_EQ_MEDIUM,
|
||||
} from "../../../data/math/nonlinear-equations";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Linear-Quadratic Systems", icon: Target },
|
||||
{ title: "Substitution with Nonlinear", icon: ArrowRight },
|
||||
{ title: "Graphical Interpretation", icon: BarChart },
|
||||
{ title: "Number of Solutions", icon: Lightbulb },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function SystemsEq2VarLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Systems of Equations in Two Variables"
|
||||
sections={SECTIONS}
|
||||
color="violet"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1: Linear-Quadratic Systems */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Linear-Quadratic Systems
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A system with one <strong>linear</strong> and one{" "}
|
||||
<strong>quadratic</strong> equation. Geometrically, this is finding
|
||||
where a line intersects a parabola.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-emerald-700">2 Solutions</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Line crosses parabola twice
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700">1 Solution</p>
|
||||
<p className="text-xs text-slate-500">Line tangent to parabola</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-rose-700">0 Solutions</p>
|
||||
<p className="text-xs text-slate-500">Line misses parabola</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Substitution with Nonlinear */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Substitution with Nonlinear Equations
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The go-to strategy: substitute the linear equation into the
|
||||
quadratic, then solve the resulting quadratic equation.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Line Meets Parabola" color="violet">
|
||||
<p>y = x + 1 and y = x² − 3</p>
|
||||
<p className="text-slate-500">Set equal: x + 1 = x² − 3</p>
|
||||
<p className="text-slate-500">x² − x − 4 = 0</p>
|
||||
<p className="text-slate-500">x = (1 ± √17) ÷ 2</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">Two intersection points</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard
|
||||
title="Example: Simplifying Before Solving"
|
||||
color="violet"
|
||||
>
|
||||
<p>y = 2x and y = x² + 2x − 3</p>
|
||||
<p className="text-slate-500">2x = x² + 2x − 3 → x² = 3</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-violet-700">x = ±√3</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Graphical Interpretation */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Graphical Interpretation
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
On the SAT, you may be shown a graph with a line and a parabola and
|
||||
asked to identify the solutions (intersection points), or asked "for
|
||||
what value of k does the system have exactly one solution?"
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<LinearQuadraticSystemWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
Each intersection point on the graph corresponds to a solution (x,
|
||||
y) of the system. Count intersection points = count solutions.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Number of Solutions */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Number of Solutions
|
||||
</h2>
|
||||
<ConceptCard color="violet">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
After substitution, you get a quadratic. Use the{" "}
|
||||
<strong>discriminant</strong> to determine how many solutions exist.
|
||||
</p>
|
||||
<FormulaBox>
|
||||
Discriminant: b² − 4ac from the resulting quadratic
|
||||
</FormulaBox>
|
||||
<div className="grid grid-cols-3 gap-3 mt-3 text-sm">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800">b² − 4ac > 0</p>
|
||||
<p className="text-slate-600">2 solutions</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800">b² − 4ac = 0</p>
|
||||
<p className="text-slate-600">1 solution</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-violet-100 text-center">
|
||||
<p className="font-bold text-violet-800">b² − 4ac < 0</p>
|
||||
<p className="text-slate-600">0 solutions</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Finding k for One Solution" color="violet">
|
||||
<p>y = kx + 2 and y = x²</p>
|
||||
<p className="text-slate-500">kx + 2 = x² → x² − kx − 2 = 0</p>
|
||||
<p className="text-slate-500">
|
||||
For exactly one solution: b² − 4ac = 0
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
k² − 4(1)(−2) = 0 → k² = −8 → no real k
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
This system always has 0 or 2 solutions, never exactly 1.
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
When the SAT asks "for what value of k does the system have
|
||||
exactly one solution," set the discriminant equal to 0 and solve
|
||||
for k.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Practice & Quiz */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{NONLINEAR_EQ_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
{NONLINEAR_EQ_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="violet" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
407
src/pages/student/lessons/SystemsEquationsLesson.tsx
Normal file
407
src/pages/student/lessons/SystemsEquationsLesson.tsx
Normal file
@ -0,0 +1,407 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Grid, RefreshCw } from "lucide-react";
|
||||
import SystemVisualizerWidget from "../../../components/lessons/SystemVisualizerWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { SYSTEMS_QUIZ_DATA } from "../../../utils/constants";
|
||||
import { Frac } from "../../../components/Math";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const SystemsEquationsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) setActiveSection(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px" },
|
||||
);
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${isActive ? "bg-white shadow-md border border-blue-100" : "hover:bg-slate-100"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isActive ? "bg-blue-600 text-white" : isPast ? "bg-blue-400 text-white" : "bg-slate-200 text-slate-500"}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-blue-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker index={0} title="Number of Solutions" icon={Grid} />
|
||||
<SectionMarker index={1} title="Solving Methods" icon={RefreshCw} />
|
||||
<SectionMarker index={2} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1: Number of Solutions */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Systems of Equations: Number of Solutions
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
A system of two linear equations represents two lines on a graph.
|
||||
The <strong>number of solutions</strong> tells you how those lines
|
||||
relate geometrically and algebraically. Every SAT test includes at
|
||||
least one question asking you to identify how many solutions a
|
||||
system has, or to find a constant that produces a specific
|
||||
outcome.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
The Three Possible Outcomes
|
||||
</h3>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-blue-900 text-white">
|
||||
<th className="p-3 text-left rounded-tl-lg">Outcome</th>
|
||||
<th className="p-3 text-left">Geometric Picture</th>
|
||||
<th className="p-3 text-left">Algebraic Condition</th>
|
||||
<th className="p-3 text-left rounded-tr-lg">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-blue-100">
|
||||
<tr className="bg-blue-100">
|
||||
<td className="p-3 font-bold text-blue-900">
|
||||
One Solution
|
||||
</td>
|
||||
<td className="p-3 text-slate-700">
|
||||
Lines intersect at exactly one point
|
||||
</td>
|
||||
<td className="p-3 text-slate-700">Different slopes</td>
|
||||
<td className="p-3 font-mono text-xs text-slate-700">
|
||||
y = 2x + 1<br />y = −x + 4
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-red-50">
|
||||
<td className="p-3 font-bold text-red-900">No Solution</td>
|
||||
<td className="p-3 text-slate-700">
|
||||
Lines are parallel — never meet
|
||||
</td>
|
||||
<td className="p-3 text-slate-700">
|
||||
Same slope, different y-intercept
|
||||
</td>
|
||||
<td className="p-3 font-mono text-xs text-slate-700">
|
||||
y = 2x + 1<br />y = 2x + 5
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-emerald-50">
|
||||
<td className="p-3 font-bold text-emerald-900">
|
||||
Infinite Solutions
|
||||
</td>
|
||||
<td className="p-3 text-slate-700">
|
||||
Same line — perfectly overlap
|
||||
</td>
|
||||
<td className="p-3 text-slate-700">
|
||||
Same slope AND same y-intercept (equations are multiples)
|
||||
</td>
|
||||
<td className="p-3 font-mono text-xs text-slate-700">
|
||||
y = 2x + 1<br />
|
||||
2y = 4x + 2
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Finding k for specific number of solutions */}
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">
|
||||
SAT Technique: Finding k for a Specific Number of Solutions
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-blue-800 mb-2">
|
||||
Example: For what value of k does 2x + ky = 6 and 4x + 2y =
|
||||
12 have infinite solutions?
|
||||
</p>
|
||||
<div className="font-mono space-y-1 text-slate-700">
|
||||
<p>
|
||||
For infinite solutions, equations must be proportional.
|
||||
</p>
|
||||
<p>
|
||||
Ratio of x-coefficients: <Frac n="4" d="2" /> = 2
|
||||
</p>
|
||||
<p>
|
||||
So all coefficients must scale by 2: k must satisfy{" "}
|
||||
<Frac n="2k" d="2" /> = 2, so k = 2.
|
||||
</p>
|
||||
<p>
|
||||
Check constants: <Frac n="12" d="6" /> = 2 ✓
|
||||
</p>
|
||||
<p className="text-blue-700 font-bold">
|
||||
k = 2 → infinite solutions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold text-red-800 mb-2">
|
||||
Example: For what value of k does 3x + 2y = 8 and kx + 4y =
|
||||
5 have no solution?
|
||||
</p>
|
||||
<div className="font-mono space-y-1 text-slate-700">
|
||||
<p>No solution → same slope, different intercept.</p>
|
||||
<p>
|
||||
Same slope means coefficient ratios match for x and y:{" "}
|
||||
<Frac n="k" d="3" /> = <Frac n="4" d="2" /> = 2
|
||||
</p>
|
||||
<p>
|
||||
So k = 6. Check constants: <Frac n="5" d="8" /> ≠ 2 ✓
|
||||
(different, confirming no solution)
|
||||
</p>
|
||||
<p className="text-red-700 font-bold">
|
||||
k = 6 → no solution
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-red-800 mb-1">
|
||||
Critical Distinction: No Solution vs. Infinite Solutions
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
<strong>No solution</strong>: coefficients are proportional but
|
||||
constants are NOT. (Same slope, different lines.)
|
||||
<br />
|
||||
<strong>Infinite solutions</strong>: coefficients AND constants
|
||||
are proportional. (Same line, just written differently.) Divide
|
||||
one equation by the other and everything must cancel cleanly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SystemVisualizerWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Solving Methods{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Solving Methods */}
|
||||
<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-6">
|
||||
Solving Methods
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-6">
|
||||
<p>
|
||||
The SAT presents systems in many formats. Choose your method based
|
||||
on what form the equations are already in — don't waste time
|
||||
converting unless necessary.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6 mb-8 space-y-5">
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
Method 1: Substitution
|
||||
</h3>
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
<strong>Best when:</strong> one variable is already isolated
|
||||
(e.g., y = 2x − 5) or easy to isolate.
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
<strong>Process:</strong> Plug one equation into the other to
|
||||
create a single-variable equation.
|
||||
</p>
|
||||
<div className="bg-blue-50 rounded-lg p-4 font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Given: y = 2x − 5 and x + y = 7</p>
|
||||
<p>Substitute y = 2x − 5 into x + y = 7:</p>
|
||||
<p>x + (2x − 5) = 7</p>
|
||||
<p>3x = 12 → x = 4</p>
|
||||
<p>y = 2(4) − 5 = 3</p>
|
||||
<p className="text-blue-700 font-bold">Solution: (4, 3)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-blue-900">
|
||||
Method 2: Elimination (Addition/Subtraction)
|
||||
</h3>
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
<strong>Best when:</strong> equations are in standard form (Ax +
|
||||
By = C) and coefficients are easy to match.
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
<strong>Process:</strong> Multiply one or both equations so one
|
||||
variable's coefficients are equal and opposite, then add the
|
||||
equations.
|
||||
</p>
|
||||
<div className="bg-blue-50 rounded-lg p-4 font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Given: 2x + y = 10 and 2x − y = 2</p>
|
||||
<p>Add equations (y terms cancel):</p>
|
||||
<p>4x = 12 → x = 3</p>
|
||||
<p>Back-substitute: 2(3) + y = 10 → y = 4</p>
|
||||
<p className="text-blue-700 font-bold">Solution: (3, 4)</p>
|
||||
</div>
|
||||
<div className="mt-3 bg-blue-50 rounded-lg p-4 font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Given: 3x + 2y = 16 and x + y = 7</p>
|
||||
<p>Multiply the second by 2: 2x + 2y = 14</p>
|
||||
<p>Subtract: (3x + 2y) − (2x + 2y) = 16 − 14</p>
|
||||
<p>x = 2, then y = 5</p>
|
||||
<p className="text-blue-700 font-bold">Solution: (2, 5)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SAT Speed Tip */}
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-5">
|
||||
<p className="font-bold text-sky-900 mb-2">
|
||||
SAT Speed Strategy: Sum/Difference Shortcut
|
||||
</p>
|
||||
<p className="text-slate-700 text-sm mb-3">
|
||||
If the SAT asks for a <em>combination</em> like x + y, or 2x −
|
||||
y, you can often get it directly by adding or subtracting the
|
||||
equations — without finding x and y individually.
|
||||
</p>
|
||||
<div className="bg-white rounded-lg p-3 font-mono text-sm space-y-1 text-slate-700">
|
||||
<p>Given: 3x + 2y = 14 and x + y = 6. Find 2x + y.</p>
|
||||
<p>Subtract eq2 from eq1: (3x + 2y) − (x + y) = 14 − 6</p>
|
||||
<p className="text-sky-700 font-bold">
|
||||
2x + y = 8 ← answer directly!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Word Problem Translation */}
|
||||
<div className="bg-white rounded-xl p-5 border border-blue-100">
|
||||
<p className="font-bold text-blue-800 mb-3">Word Problem Setup</p>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
Most SAT system word problems follow this template: define two
|
||||
variables, write two equations (one for each constraint), solve.
|
||||
</p>
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-sm">
|
||||
<p className="text-slate-700 mb-2 italic">
|
||||
"A store sells pens for $2 and notebooks for $5. A customer
|
||||
buys 8 items total and spends $28. How many of each did they
|
||||
buy?"
|
||||
</p>
|
||||
<div className="font-mono space-y-1 text-slate-700">
|
||||
<p>Let p = pens, n = notebooks</p>
|
||||
<p>p + n = 8 (total items)</p>
|
||||
<p>2p + 5n = 28 (total cost)</p>
|
||||
<p>From first: p = 8 − n. Substitute: 2(8 − n) + 5n = 28</p>
|
||||
<p>
|
||||
16 − 2n + 5n = 28 → 3n = 12 →{" "}
|
||||
<strong className="text-blue-700">
|
||||
n = 4 notebooks, p = 4 pens
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-blue-600 font-bold hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Quiz */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[2] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{SYSTEMS_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-blue-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-blue-900 font-bold rounded-full hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemsEquationsLesson;
|
||||
206
src/pages/student/lessons/SystemsLinearEqLesson.tsx
Normal file
206
src/pages/student/lessons/SystemsLinearEqLesson.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
import React from "react";
|
||||
import { Layers, ArrowRight, Hash, Lightbulb, BookOpen } from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import SystemVisualizerWidget from "../../../components/lessons/SystemVisualizerWidget";
|
||||
import {
|
||||
SYSTEMS_EASY,
|
||||
SYSTEMS_MEDIUM,
|
||||
} from "../../../data/math/systems-linear-equations";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Graphical Solutions", icon: Layers },
|
||||
{ title: "Substitution Method", icon: ArrowRight },
|
||||
{ title: "Elimination Method", icon: Hash },
|
||||
{ title: "Number of Solutions", icon: Lightbulb },
|
||||
{ title: "Word Problems", icon: Layers },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function SystemsLinearEqLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Systems of Linear Equations"
|
||||
sections={SECTIONS}
|
||||
color="blue"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Graphical Solutions
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>system of equations</strong> is two or more equations with
|
||||
the same variables. The <strong>solution</strong> is the point (x,
|
||||
y) where the lines intersect.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-emerald-50/80 border border-emerald-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-emerald-700">One Solution</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Lines intersect at one point
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50/80 border border-amber-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-amber-700">No Solution</p>
|
||||
<p className="text-xs text-slate-500">Lines are parallel</p>
|
||||
</div>
|
||||
<div className="bg-blue-50/80 border border-blue-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-blue-700">Infinite</p>
|
||||
<p className="text-xs text-slate-500">Same line</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<div className="mt-6">
|
||||
<SystemVisualizerWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Substitution Method
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Best when one variable is already isolated or easy to isolate:
|
||||
</p>
|
||||
<div className="space-y-2 mt-3 text-sm">
|
||||
<p>1. Solve one equation for one variable</p>
|
||||
<p>2. Substitute into the other equation</p>
|
||||
<p>3. Solve for the remaining variable</p>
|
||||
<p>4. Back-substitute to find the other</p>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Substitution" color="blue">
|
||||
<p>y = 2x + 1 and 3x + y = 11</p>
|
||||
<p className="text-slate-500">Substitute: 3x + (2x + 1) = 11</p>
|
||||
<p className="text-slate-500">5x + 1 = 11 → 5x = 10 → x = 2</p>
|
||||
<p className="text-slate-500">
|
||||
y = 2(2) + 1 ={" "}
|
||||
<strong className="text-blue-700">5. Solution: (2, 5)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Elimination Method
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Best when coefficients can be easily matched. Add or subtract
|
||||
equations to eliminate one variable.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Elimination" color="blue">
|
||||
<p>2x + 3y = 12 and 4x − 3y = 6</p>
|
||||
<p className="text-slate-500">Add equations: 6x = 18 → x = 3</p>
|
||||
<p className="text-slate-500">2(3) + 3y = 12 → 3y = 6 → y = 2</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-blue-700">Solution: (3, 2)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<ExampleCard title="Example: Multiply First" color="blue">
|
||||
<p>3x + 2y = 7 and 5x + 3y = 12</p>
|
||||
<p className="text-slate-500">Multiply eq1 by 3: 9x + 6y = 21</p>
|
||||
<p className="text-slate-500">
|
||||
Multiply eq2 by −2: −10x − 6y = −24
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
Add: −x = −3 → <strong className="text-blue-700">x = 3</strong>,
|
||||
then y = −1
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
SAT shortcut: Sometimes you can add/subtract equations directly to
|
||||
get the expression they ask for (like "3x − y") without solving
|
||||
for x and y individually.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Number of Solutions
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Compare the ratios of coefficients:
|
||||
</p>
|
||||
<div className="space-y-2 mt-3">
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-sm">
|
||||
<p>
|
||||
<strong>One solution:</strong> <Frac n="a₁" d="a₂" /> ≠{" "}
|
||||
<Frac n="b₁" d="b₂" /> (different slopes)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-sm">
|
||||
<p>
|
||||
<strong>No solution:</strong> <Frac n="a₁" d="a₂" /> ={" "}
|
||||
<Frac n="b₁" d="b₂" /> ≠ <Frac n="c₁" d="c₂" /> (parallel lines)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 border border-blue-100 text-sm">
|
||||
<p>
|
||||
<strong>Infinite:</strong> <Frac n="a₁" d="a₂" /> ={" "}
|
||||
<Frac n="b₁" d="b₂" /> = <Frac n="c₁" d="c₂" /> (same line)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Word Problems
|
||||
</h2>
|
||||
<ConceptCard color="blue">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Define variables, write two equations from the given info, then
|
||||
solve.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Ticket Problem" color="blue">
|
||||
<p>
|
||||
Adult tickets cost $8, child tickets $5. Total: 200 tickets, $1,340
|
||||
revenue.
|
||||
</p>
|
||||
<p className="text-slate-500">a + c = 200 and 8a + 5c = 1340</p>
|
||||
<p className="text-slate-500">
|
||||
From eq1: a = 200 − c. Substitute: 8(200 − c) + 5c = 1340
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
1600 − 8c + 5c = 1340 → −3c = −260 → c = 86.67... (round in context)
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{SYSTEMS_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
{SYSTEMS_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="blue" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
711
src/pages/student/lessons/TrigLesson.tsx
Normal file
711
src/pages/student/lessons/TrigLesson.tsx
Normal file
@ -0,0 +1,711 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { ArrowDown, Check, BookOpen, Target, Calculator } from "lucide-react";
|
||||
import { Frac } from "../../../components/Math";
|
||||
import UnitCircleWidget from "../../../components/lessons/UnitCircleWidget";
|
||||
import Quiz from "../../../components/lessons/Quiz";
|
||||
import { TRIG_QUIZ_DATA } from "../../../utils/constants";
|
||||
|
||||
// --- Interactive Trig Ratios Component (Inline for simplicity or could be separate) ---
|
||||
const InteractiveTrigRatiosWidget = () => {
|
||||
const [angle, setAngle] = useState(30);
|
||||
const radius = 200;
|
||||
const cx = 50,
|
||||
cy = 250; // Bottom-left corner
|
||||
|
||||
// Calculate triangle vertex
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
const x = radius * Math.cos(rad);
|
||||
const y = radius * Math.sin(rad);
|
||||
const px = cx + x;
|
||||
const py = cy - y;
|
||||
|
||||
// Interaction
|
||||
const handleDrag = (e: React.MouseEvent) => {
|
||||
const rect = (e.target as Element).closest("svg")?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
const dx = mx - cx;
|
||||
const dy = cy - my;
|
||||
let deg = (Math.atan2(dy, dx) * 180) / Math.PI;
|
||||
deg = Math.max(10, Math.min(80, deg)); // Constrain angle
|
||||
setAngle(deg);
|
||||
};
|
||||
|
||||
const isDragging = useRef(false);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex flex-col md:flex-row gap-8 items-center">
|
||||
<svg
|
||||
width="300"
|
||||
height="280"
|
||||
className="select-none cursor-pointer"
|
||||
onMouseDown={() => (isDragging.current = true)}
|
||||
onMouseMove={(e) => {
|
||||
if (isDragging.current) handleDrag(e);
|
||||
}}
|
||||
onMouseUp={() => (isDragging.current = false)}
|
||||
onMouseLeave={() => (isDragging.current = false)}
|
||||
>
|
||||
{/* Axes/Grid context */}
|
||||
<line
|
||||
x1={cx}
|
||||
y1={cy}
|
||||
x2={cx + 250}
|
||||
y2={cy}
|
||||
stroke="#cbd5e1"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
{/* Triangle */}
|
||||
<path
|
||||
d={`M ${cx} ${cy} L ${px} ${cy} L ${px} ${py} Z`}
|
||||
fill="rgba(224, 242, 254, 0.5)"
|
||||
stroke="none"
|
||||
/>
|
||||
<line
|
||||
x1={cx}
|
||||
y1={cy}
|
||||
x2={px}
|
||||
y2={py}
|
||||
stroke="#0f172a"
|
||||
strokeWidth="2"
|
||||
/>{" "}
|
||||
{/* Hypotenuse */}
|
||||
<line
|
||||
x1={cx}
|
||||
y1={cy}
|
||||
x2={px}
|
||||
y2={cy}
|
||||
stroke="#0f172a"
|
||||
strokeWidth="2"
|
||||
/>{" "}
|
||||
{/* Adj */}
|
||||
<line
|
||||
x1={px}
|
||||
y1={cy}
|
||||
x2={px}
|
||||
y2={py}
|
||||
stroke="#0f172a"
|
||||
strokeWidth="2"
|
||||
/>{" "}
|
||||
{/* Opp */}
|
||||
{/* Angle Arc */}
|
||||
<path
|
||||
d={`M ${cx + 40} ${cy} A 40 40 0 0 0 ${cx + 40 * Math.cos(rad)} ${cy - 40 * Math.sin(rad)}`}
|
||||
fill="none"
|
||||
stroke="#0ea5e9"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text x={cx + 50} y={cy - 10} className="font-bold fill-sky-600">
|
||||
{Math.round(angle)}°
|
||||
</text>
|
||||
{/* Labels */}
|
||||
<text x={px + 10} y={cy - y / 2} className="font-bold fill-rose-500">
|
||||
Opposite
|
||||
</text>
|
||||
<text
|
||||
x={cx + x / 2}
|
||||
y={cy + 20}
|
||||
className="font-bold fill-indigo-500"
|
||||
>
|
||||
Adjacent
|
||||
</text>
|
||||
<text
|
||||
x={cx + x / 2 - 20}
|
||||
y={cy - y / 2 - 10}
|
||||
className="font-bold fill-slate-500"
|
||||
>
|
||||
Hypotenuse (r)
|
||||
</text>
|
||||
{/* Drag Handle */}
|
||||
<circle
|
||||
cx={px}
|
||||
cy={py}
|
||||
r={8}
|
||||
fill="#0ea5e9"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
className="cursor-grab hover:scale-110 transition-transform"
|
||||
/>
|
||||
{/* Right Angle */}
|
||||
<rect
|
||||
x={px - 20}
|
||||
y={cy - 20}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke="#94a3b8"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="flex-1 space-y-4 font-mono">
|
||||
<div className="p-3 bg-rose-50 border border-rose-100 rounded-lg">
|
||||
<div className="text-xs font-bold text-rose-800 uppercase">
|
||||
Sine (Opp/Hyp)
|
||||
</div>
|
||||
<div className="text-xl font-bold text-rose-900">
|
||||
{Math.sin(rad).toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-indigo-50 border border-indigo-100 rounded-lg">
|
||||
<div className="text-xs font-bold text-indigo-800 uppercase">
|
||||
Cosine (Adj/Hyp)
|
||||
</div>
|
||||
<div className="text-xl font-bold text-indigo-900">
|
||||
{Math.cos(rad).toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 border border-slate-200 rounded-lg">
|
||||
<div className="text-xs font-bold text-slate-800 uppercase">
|
||||
Tangent (Opp/Adj)
|
||||
</div>
|
||||
<div className="text-xl font-bold text-slate-900">
|
||||
{Math.tan(rad).toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
Drag the blue vertex to change θ!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Interactive Special Triangles Component ---
|
||||
const InteractiveSpecialTrianglesWidget = () => {
|
||||
const [scale, setScale] = useState(100);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-4 bg-slate-100 p-4 rounded-lg">
|
||||
<span className="font-bold text-slate-600">Scale (x):</span>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="150"
|
||||
value={scale}
|
||||
onChange={(e) => setScale(parseInt(e.target.value))}
|
||||
className="flex-1 h-2 bg-slate-300 rounded-lg appearance-none cursor-pointer accent-sky-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 45-45-90 */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 text-center flex flex-col items-center">
|
||||
<h4 className="font-bold text-xl mb-4 text-slate-800">
|
||||
45-45-90 Triangle
|
||||
</h4>
|
||||
<div className="h-64 flex items-end justify-center pb-4">
|
||||
<svg width="200" height="200" className="overflow-visible">
|
||||
<path
|
||||
d={`M 0 200 L ${scale} 200 L ${scale} ${200 - scale} Z`}
|
||||
transform="translate(20, -20)"
|
||||
fill="rgba(14, 165, 233, 0.1)"
|
||||
stroke="#0ea5e9"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
{/* Labels */}
|
||||
<g transform="translate(20, -20)">
|
||||
<text
|
||||
x={scale / 2}
|
||||
y={220}
|
||||
textAnchor="middle"
|
||||
className="font-bold fill-slate-700"
|
||||
>
|
||||
x = {Math.round(scale / 2)}
|
||||
</text>
|
||||
<text
|
||||
x={scale + 10}
|
||||
y={200 - scale / 2}
|
||||
className="font-bold fill-slate-700"
|
||||
>
|
||||
x = {Math.round(scale / 2)}
|
||||
</text>
|
||||
<text
|
||||
x={scale / 2 - 10}
|
||||
y={200 - scale / 2}
|
||||
textAnchor="end"
|
||||
className="font-bold fill-sky-600"
|
||||
>
|
||||
x√2 ≈ {Math.round((scale / 2) * 1.414)}
|
||||
</text>
|
||||
<text x={20} y={190} className="text-xs fill-slate-400">
|
||||
45°
|
||||
</text>
|
||||
<text
|
||||
x={scale - 10}
|
||||
y={200 - scale + 20}
|
||||
className="text-xs fill-slate-400"
|
||||
>
|
||||
45°
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 font-medium">
|
||||
Sides: 1 : 1 : √2
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 30-60-90 */}
|
||||
<div className="bg-white p-6 rounded-xl border border-slate-200 text-center flex flex-col items-center">
|
||||
<h4 className="font-bold text-xl mb-4 text-slate-800">
|
||||
30-60-90 Triangle
|
||||
</h4>
|
||||
<div className="h-64 flex items-end justify-center pb-4">
|
||||
<svg width="200" height="200" className="overflow-visible">
|
||||
{/* Base is x, Height is x√3 */}
|
||||
<path
|
||||
d={`M 0 200 L ${scale} 200 L ${scale} ${200 - scale * 1.732} Z`}
|
||||
transform="translate(20, 0)"
|
||||
fill="rgba(14, 165, 233, 0.1)"
|
||||
stroke="#0ea5e9"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<g transform="translate(20, 0)">
|
||||
<text
|
||||
x={scale / 2}
|
||||
y={220}
|
||||
textAnchor="middle"
|
||||
className="font-bold fill-slate-700"
|
||||
>
|
||||
x = {Math.round(scale / 2)}
|
||||
</text>
|
||||
<text
|
||||
x={scale + 10}
|
||||
y={200 - (scale * 1.732) / 2}
|
||||
className="font-bold fill-slate-700"
|
||||
>
|
||||
x√3 ≈ {Math.round((scale / 2) * 1.732)}
|
||||
</text>
|
||||
<text
|
||||
x={scale / 2 - 10}
|
||||
y={200 - (scale * 1.732) / 2}
|
||||
textAnchor="end"
|
||||
className="font-bold fill-sky-600"
|
||||
>
|
||||
2x = {scale}
|
||||
</text>
|
||||
<text
|
||||
x={scale - 20}
|
||||
y={200 - scale * 1.732 + 30}
|
||||
className="text-xs fill-slate-400"
|
||||
>
|
||||
30°
|
||||
</text>
|
||||
<text x={20} y={190} className="text-xs fill-slate-400">
|
||||
60°
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 font-medium">
|
||||
Sides: 1 : √3 : 2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const TrigLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
};
|
||||
|
||||
// Scroll Spy Effect
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const index = sectionsRef.current.indexOf(
|
||||
entry.target as HTMLElement,
|
||||
);
|
||||
if (index !== -1) {
|
||||
setActiveSection(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -60% 0px",
|
||||
},
|
||||
);
|
||||
|
||||
sectionsRef.current.forEach((section) => {
|
||||
if (section) observer.observe(section);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const SectionMarker = ({
|
||||
index,
|
||||
title,
|
||||
icon: Icon,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
icon: any;
|
||||
}) => {
|
||||
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 transition-all ${
|
||||
isActive
|
||||
? "bg-white shadow-md border border-sky-100"
|
||||
: "hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
|
||||
isActive
|
||||
? "bg-sky-600 text-white"
|
||||
: isPast
|
||||
? "bg-sky-400 text-white"
|
||||
: "bg-slate-200 text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{isPast ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Icon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p
|
||||
className={`text-sm font-bold ${isActive ? "text-sky-900" : "text-slate-600"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
<aside className="w-full lg:w-64 lg:fixed lg:top-20 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">
|
||||
<SectionMarker
|
||||
index={0}
|
||||
title="Unit Circle & Radians"
|
||||
icon={Target}
|
||||
/>
|
||||
<SectionMarker index={1} title="Trig Ratios" icon={Calculator} />
|
||||
<SectionMarker index={2} title="Special Triangles" icon={BookOpen} />
|
||||
<SectionMarker index={3} title="Practice" icon={BookOpen} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
{/* Section 1 */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[0] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
|
||||
Radians & The Unit Circle
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
The unit circle connects geometry and trigonometry. In this
|
||||
circle, the radius is always <strong>1</strong>. This means the
|
||||
coordinates of any point on the circle are simply{" "}
|
||||
<strong>(cos θ, sin θ)</strong>.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<strong>Radians</strong> are another way to measure angles. A full
|
||||
circle (360°) is <strong>2π radians</strong>.
|
||||
<br />
|
||||
Use the interactive tool below to explore how angles, radians, and
|
||||
coordinates relate.
|
||||
<br />
|
||||
<span className="text-sm bg-sky-100 text-sky-800 px-2 py-1 rounded font-bold">
|
||||
Try dragging the point or clicking the special angle buttons!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UnitCircleWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(1)}
|
||||
className="mt-12 group flex items-center text-sky-600 font-bold hover:text-sky-800 transition-colors"
|
||||
>
|
||||
Next: SOH CAH TOA{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 2 */}
|
||||
<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-6">
|
||||
Trigonometric Ratios
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
In a right triangle, the three main trig ratios connect each acute
|
||||
angle to a pair of sides. Memorize <strong>SOH CAH TOA</strong> —
|
||||
it solves the majority of SAT trig problems.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-4 text-center">
|
||||
<div className="text-2xl font-bold text-rose-700 mb-1">SOH</div>
|
||||
<div className="font-bold text-rose-900">
|
||||
sin θ = <Frac n="Opp" d="Hyp" />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
Opposite over Hypotenuse
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-4 text-center">
|
||||
<div className="text-2xl font-bold text-indigo-700 mb-1">
|
||||
CAH
|
||||
</div>
|
||||
<div className="font-bold text-indigo-900">
|
||||
cos θ = <Frac n="Adj" d="Hyp" />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
Adjacent over Hypotenuse
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-100 border border-slate-300 rounded-xl p-4 text-center">
|
||||
<div className="text-2xl font-bold text-slate-700 mb-1">
|
||||
TOA
|
||||
</div>
|
||||
<div className="font-bold text-slate-900">
|
||||
tan θ = <Frac n="Opp" d="Adj" />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
Opposite over Adjacent
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worked Example */}
|
||||
<div className="mt-4 bg-sky-50 border border-sky-200 rounded-xl p-5 text-sm">
|
||||
<p className="font-bold text-sky-900 mb-2">
|
||||
Worked Example — Using SOH CAH TOA
|
||||
</p>
|
||||
<p className="text-slate-700 mb-2">
|
||||
In a right triangle, the side opposite to angle θ is 5, and the
|
||||
hypotenuse is 13. Find sin θ, cos θ, and tan θ.
|
||||
</p>
|
||||
<div className="bg-white rounded-lg p-3 font-mono text-xs text-slate-700 space-y-1">
|
||||
<p>
|
||||
First find the adjacent side: a² + 5² = 13² → a² = 169 − 25 =
|
||||
144 → a = 12
|
||||
</p>
|
||||
<p className="text-rose-700 font-bold">
|
||||
sin θ = <Frac n="5" d="13" /> ≈ 0.385
|
||||
</p>
|
||||
<p className="text-indigo-700 font-bold">
|
||||
cos θ = <Frac n="12" d="13" /> ≈ 0.923
|
||||
</p>
|
||||
<p className="text-slate-700 font-bold">
|
||||
tan θ = <Frac n="5" d="12" /> ≈ 0.417
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 bg-sky-50 border-l-4 border-sky-500 p-4 rounded-r-lg text-sm">
|
||||
<strong className="text-sky-900">
|
||||
Co-function Identity (SAT favourite!):
|
||||
</strong>
|
||||
<span className="text-slate-700">
|
||||
{" "}
|
||||
sin(θ) = cos(90° − θ). So sin(30°) = cos(60°). If an angle and
|
||||
its complement appear in the same triangle, their sin and cos
|
||||
are swapped. This is tested as: "sin(x°) = cos(y°), and x + y =
|
||||
90. Find y if x = 32."
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-slate-500">
|
||||
Drag the blue point below to see how the three values change with
|
||||
the angle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InteractiveTrigRatiosWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(2)}
|
||||
className="mt-12 group flex items-center text-sky-600 font-bold hover:text-sky-800 transition-colors"
|
||||
>
|
||||
Next: Special Triangles{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 3 */}
|
||||
<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">
|
||||
Special Right Triangles
|
||||
</h2>
|
||||
<div className="prose prose-slate text-lg text-slate-600 mb-8">
|
||||
<p>
|
||||
These two triangle types appear on every SAT. You must know their
|
||||
exact side ratios — no calculator needed.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-5">
|
||||
<p className="font-bold text-sky-900 mb-2">45-45-90 Triangle</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-sky-700 font-bold mb-2">
|
||||
1 : 1 : √2
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">
|
||||
If leg = x, then hypotenuse = x√2.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Isosceles right triangle. Appears in squares cut diagonally.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-5">
|
||||
<p className="font-bold text-indigo-900 mb-2">
|
||||
30-60-90 Triangle
|
||||
</p>
|
||||
<div className="font-mono text-center bg-white py-2 rounded text-indigo-700 font-bold mb-2">
|
||||
1 : √3 : 2
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">
|
||||
Short leg = x, long leg = x√3, hypotenuse = 2x.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Half an equilateral triangle. The 2 is always the hypotenuse.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto mt-4 rounded-xl border border-slate-200">
|
||||
<table className="w-full text-sm border-collapse text-center">
|
||||
<thead>
|
||||
<tr className="bg-slate-800 text-white">
|
||||
<th className="p-2">Angle</th>
|
||||
<th className="p-2">sin θ</th>
|
||||
<th className="p-2">cos θ</th>
|
||||
<th className="p-2">tan θ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
<tr className="bg-white">
|
||||
<td className="p-2 font-bold">30°</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="1" d="2" />
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="√3" d="2" />
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="√3" d="3" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="p-2 font-bold">45°</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="√2" d="2" />
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="√2" d="2" />
|
||||
</td>
|
||||
<td className="p-2 font-mono text-center">1</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-2 font-bold">60°</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="√3" d="2" />
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<Frac n="1" d="2" />
|
||||
</td>
|
||||
<td className="p-2 font-mono text-center">√3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-3 bg-sky-50 border border-sky-200 rounded-xl p-4 text-sm">
|
||||
<p className="font-bold text-sky-900 mb-1">
|
||||
Memory Trick for the Table
|
||||
</p>
|
||||
<p className="text-slate-700">
|
||||
For sin: 30° → ½, 45° → √2÷2, 60° → √3÷2. Notice sin{" "}
|
||||
<em>increases</em> as the angle increases (from ½ to 1). For
|
||||
cos, it's the reverse — cos 30° = √3÷2, cos 60° = ½. And tan 45°
|
||||
= 1 because opposite = adjacent in a 45-45-90 triangle.
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-slate-500">
|
||||
Use the slider below to scale the triangles and confirm the side
|
||||
proportions hold.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InteractiveSpecialTrianglesWidget />
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection(3)}
|
||||
className="mt-12 group flex items-center text-sky-600 font-bold hover:text-sky-800 transition-colors"
|
||||
>
|
||||
Next: Practice Quiz{" "}
|
||||
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Section 4 */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
sectionsRef.current[3] = el;
|
||||
}}
|
||||
className="min-h-screen flex flex-col justify-center"
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-8">
|
||||
Practice Time
|
||||
</h2>
|
||||
{TRIG_QUIZ_DATA.map((quiz, idx) => (
|
||||
<div key={quiz.id} className="mb-12">
|
||||
<Quiz data={quiz} />
|
||||
</div>
|
||||
))}
|
||||
<div className="p-8 bg-sky-900 rounded-2xl text-white text-center mt-12">
|
||||
<h3 className="text-2xl font-bold mb-4">Topic Mastered!</h3>
|
||||
<button
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-white text-sky-900 font-bold rounded-full hover:bg-sky-50 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrigLesson;
|
||||
272
src/pages/student/lessons/TwoVariableDataLesson.tsx
Normal file
272
src/pages/student/lessons/TwoVariableDataLesson.tsx
Normal file
@ -0,0 +1,272 @@
|
||||
import {
|
||||
BarChart,
|
||||
TrendingUp,
|
||||
Target,
|
||||
Layers,
|
||||
Hash,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import LessonShell, {
|
||||
ConceptCard,
|
||||
FormulaBox,
|
||||
ExampleCard,
|
||||
TipCard,
|
||||
PracticeFromDataset,
|
||||
} from "../../../components/lessons/LessonShell";
|
||||
import ScatterplotInteractiveWidget from "../../../components/lessons/ScatterplotInteractiveWidget";
|
||||
import {
|
||||
TWO_VAR_DATA_EASY,
|
||||
TWO_VAR_DATA_MEDIUM,
|
||||
} from "../../../data/math/two-variable-data";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
const SECTIONS = [
|
||||
{ title: "Reading Scatterplots", icon: BarChart },
|
||||
{ title: "Line of Best Fit", icon: TrendingUp },
|
||||
{ title: "Correlation Coefficient", icon: Target },
|
||||
{ title: "Linear & Exponential Models", icon: Layers },
|
||||
{ title: "Residuals", icon: Hash },
|
||||
{ title: "Practice & Quiz", icon: BookOpen },
|
||||
];
|
||||
|
||||
export default function TwoVariableDataLesson({ onFinish }: LessonProps) {
|
||||
return (
|
||||
<LessonShell
|
||||
title="Two-Variable Data: Models & Scatterplots"
|
||||
sections={SECTIONS}
|
||||
color="amber"
|
||||
onFinish={onFinish}
|
||||
>
|
||||
{/* Section 1 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Reading Scatterplots
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A scatterplot displays two quantitative variables as points (x, y).
|
||||
Describe the relationship by its <strong>direction</strong>,{" "}
|
||||
<strong>form</strong>, and <strong>strength</strong>.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-emerald-700 text-sm">Positive</p>
|
||||
<p className="text-xs text-slate-500">As x ↑, y ↑</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-rose-700 text-sm">Negative</p>
|
||||
<p className="text-xs text-slate-500">As x ↑, y ↓</p>
|
||||
</div>
|
||||
<div className="bg-slate-100 border border-slate-200 rounded-xl p-3 text-center">
|
||||
<p className="font-bold text-slate-700 text-sm">None</p>
|
||||
<p className="text-xs text-slate-500">No clear pattern</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
On the SAT, you'll read values from scatterplots, identify trends,
|
||||
estimate data points, and interpret results in context.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Line of Best Fit
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The <strong>line of best fit</strong> (regression line) best
|
||||
represents the trend. Use it to make predictions:{" "}
|
||||
<strong>interpolation</strong> (within data range) is reliable,{" "}
|
||||
<strong>extrapolation</strong> (beyond data range) is risky.
|
||||
</p>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Prediction" color="amber">
|
||||
<p>Line of best fit: y = 2.5x + 10</p>
|
||||
<p className="text-slate-500">Predict y when x = 8:</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">y = 2.5(8) + 10 = 30</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-6">
|
||||
<ScatterplotInteractiveWidget />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
The y-intercept of the best fit line represents the predicted
|
||||
y-value when x = 0. The slope represents the predicted change in y
|
||||
for each 1-unit increase in x.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Correlation Coefficient (r)
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
The correlation coefficient <strong>r</strong> measures the strength
|
||||
and direction of a <strong>linear</strong> relationship. It ranges
|
||||
from −1 to 1.
|
||||
</p>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-amber-100 text-amber-900">
|
||||
<th className="border border-amber-300 px-3 py-2 font-bold">
|
||||
|r| Value
|
||||
</th>
|
||||
<th className="border border-amber-300 px-3 py-2 font-bold">
|
||||
Strength
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
0.8 – 1.0
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold text-emerald-700">
|
||||
Strong
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-slate-50">
|
||||
<td className="border border-slate-200 px-3 py-2">
|
||||
0.5 – 0.8
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold text-amber-700">
|
||||
Moderate
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="border border-slate-200 px-3 py-2">0 – 0.5</td>
|
||||
<td className="border border-slate-200 px-3 py-2 font-semibold text-rose-700">
|
||||
Weak
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<TipCard type="warning">
|
||||
<p className="text-slate-700">
|
||||
Correlation does NOT imply causation! A strong r only means a linear
|
||||
relationship exists — it doesn't tell you WHY.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
|
||||
{/* Section 4 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Linear vs Exponential Models
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Choose the model based on how the data changes:
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<p className="font-bold text-blue-800 mb-1">
|
||||
Linear (y = mx + b)
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Constant <strong>differences</strong> between consecutive
|
||||
y-values
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-violet-50 border border-violet-200 rounded-xl p-4">
|
||||
<p className="font-bold text-violet-800 mb-1">
|
||||
Exponential (y = a × b<sup>x</sup>)
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
Constant <strong>ratios</strong> between consecutive y-values
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example: Identify Model" color="amber">
|
||||
<p>x: 0, 1, 2, 3 → y: 100, 150, 225, 337.5</p>
|
||||
<p className="text-slate-500">
|
||||
Ratios: 150÷100 = 1.5, 225÷150 = 1.5, 337.5÷225 = 1.5
|
||||
</p>
|
||||
<p className="text-slate-500">
|
||||
<strong className="text-amber-700">
|
||||
Constant ratio → Exponential model: y = 100(1.5)<sup>x</sup>
|
||||
</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
</div>
|
||||
|
||||
{/* Section 5 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Residuals
|
||||
</h2>
|
||||
<ConceptCard color="amber">
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
A <strong>residual</strong> measures how far a data point is from
|
||||
the predicted value. A good model has residuals that are randomly
|
||||
scattered around zero.
|
||||
</p>
|
||||
<FormulaBox>Residual = Actual y − Predicted y</FormulaBox>
|
||||
<div className="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3">
|
||||
<p className="font-bold text-emerald-800 text-sm">
|
||||
Positive Residual
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Actual > Predicted (point above line)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-3">
|
||||
<p className="font-bold text-rose-800 text-sm">
|
||||
Negative Residual
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Actual < Predicted (point below line)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConceptCard>
|
||||
<ExampleCard title="Example" color="amber">
|
||||
<p>Best fit predicts y = 45 when x = 10, but actual y = 48</p>
|
||||
<p className="text-slate-500">
|
||||
Residual = 48 − 45 ={" "}
|
||||
<strong className="text-amber-700">+3 (above the line)</strong>
|
||||
</p>
|
||||
</ExampleCard>
|
||||
<div className="mt-4">
|
||||
<TipCard type="tip">
|
||||
<p className="text-slate-700">
|
||||
If a residual plot shows a curved pattern, the linear model is NOT
|
||||
appropriate. Random scatter = good fit.
|
||||
</p>
|
||||
</TipCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 6 */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 mb-6">
|
||||
Practice & Quiz
|
||||
</h2>
|
||||
{TWO_VAR_DATA_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
{TWO_VAR_DATA_MEDIUM.slice(0, 1).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="amber" />
|
||||
))}
|
||||
</div>
|
||||
</LessonShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user