feat(lessons): add lessons from client db

This commit is contained in:
shafin-r
2026-03-01 20:24:14 +06:00
parent 2eaf77e13c
commit 2a00c44157
152 changed files with 74587 additions and 222 deletions

View File

@ -1,11 +1,25 @@
import { useAuthStore } from "../../stores/authStore";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { api } from "../../utils/api";
import { type Lesson } from "../../types/lesson";
import { LessonSkeleton } from "../../components/LessonSkeleton";
import { type Lesson, type LessonMetadata } from "../../types/lesson";
import { LessonModal } from "../../components/LessonModal";
import { BookOpen, Calculator } from "lucide-react";
import {
BookOpen,
Calculator,
Video,
ChevronRight,
Search,
X,
Play,
} from "lucide-react";
import { truncate } from "../../lib/utils";
import { EBRW_LESSONS, MATH_LESSONS } from "../../utils/constants";
import { renderLessonIcon } from "../../components/RenderLessonIcon";
// ─── Types ────────────────────────────────────────────────────────────────────
type VideoSubTab = "rw" | "math";
// ─── Decorative dots ─────────────────────────────────────────────────────────
const DOTS = [
{ size: 10, color: "#f97316", top: "6%", left: "4%", delay: "0s" },
{ size: 7, color: "#a855f7", top: "25%", left: "2%", delay: "1.2s" },
@ -15,6 +29,7 @@ const DOTS = [
{ size: 9, color: "#eab308", top: "75%", right: "5%", delay: "0.4s" },
];
// ─── Styles ───────────────────────────────────────────────────────────────────
const STYLES = `
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
@ -26,126 +41,421 @@ const STYLES = `
overflow-x: hidden;
}
.ls-blob { position: fixed; pointer-events: none; z-index: 0; filter: blur(48px); opacity: 0.35; }
.ls-blob { position:fixed; pointer-events:none; z-index:0; filter:blur(48px); opacity:0.35; }
.ls-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:lsWobble1 14s ease-in-out infinite; }
.ls-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:lsWobble2 16s ease-in-out infinite; }
.ls-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:lsWobble1 18s ease-in-out infinite reverse; }
.ls-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:lsWobble2 12s ease-in-out infinite; }
@keyframes lsWobble1 {
0%,100%{border-radius:60% 40% 70% 30%/50% 60% 40% 50%;transform:translate(0,0) rotate(0deg);}
50%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(12px,16px) rotate(8deg);}
0%,100%{ border-radius:60% 40% 70% 30%/50% 60% 40% 50%; transform:translate(0,0) rotate(0deg); }
50% { border-radius:40% 60% 30% 70%/60% 40% 60% 40%; transform:translate(12px,16px) rotate(8deg); }
}
@keyframes lsWobble2 {
0%,100%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(0,0) rotate(0deg);}
50%{border-radius:60% 40% 70% 30%/40% 60% 40% 60%;transform:translate(-10px,12px) rotate(-6deg);}
0%,100%{ border-radius:40% 60% 30% 70%/60% 40% 60% 40%; transform:translate(0,0) rotate(0deg); }
50% { border-radius:60% 40% 70% 30%/40% 60% 40% 60%; transform:translate(-10px,12px) rotate(-6deg); }
}
.ls-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:lsFloat 7s ease-in-out infinite; }
.ls-dot { position:fixed; border-radius:50%; pointer-events:none; z-index:0; opacity:0.3; animation:lsFloat 7s ease-in-out infinite; }
@keyframes lsFloat {
0%,100%{transform:translateY(0) rotate(0deg);}
50%{transform:translateY(-12px) rotate(180deg);}
0%,100%{ transform:translateY(0) rotate(0deg); }
50% { transform:translateY(-12px) rotate(180deg); }
}
.ls-inner {
position: relative; z-index: 1;
max-width: 680px; margin: 0 auto;
padding: 2rem 1.25rem 4rem;
display: flex; flex-direction: column; gap: 1.25rem;
position:relative; z-index:1;
max-width:680px; margin:0 auto;
padding:2rem 1.25rem 6rem;
display:flex; flex-direction:column; gap:1.5rem;
}
@keyframes lsPopIn {
from { opacity:0; transform: scale(0.92) translateY(12px); }
to { opacity:1; transform: scale(1) translateY(0); }
from { opacity:0; transform:scale(0.92) translateY(12px); }
to { opacity:1; transform:scale(1) translateY(0); }
}
.ls-anim { animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
.ls-anim-1 { animation-delay: 0.05s; }
.ls-anim-2 { animation-delay: 0.1s; }
.ls-anim-3 { animation-delay: 0.15s; }
.ls-anim { animation:lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
.ls-anim-1 { animation-delay:0.05s; }
.ls-anim-2 { animation-delay:0.1s; }
.ls-anim-3 { animation-delay:0.15s; }
/* Header */
.ls-header { animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
.ls-title { font-size: 1.8rem; font-weight: 900; color: #1e1b4b; letter-spacing: -0.02em; }
.ls-sub {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.85rem; font-weight: 600; color: #9ca3af; margin-top: 0.25rem;
line-height: 1.5; max-width: 420px;
.ls-header { animation:lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
.ls-title { font-size:1.8rem; font-weight:900; color:#1e1b4b; letter-spacing:-0.02em; }
.ls-sub {
font-family:'Nunito Sans',sans-serif;
font-size:0.85rem; font-weight:600; color:#9ca3af; margin-top:0.25rem;
line-height:1.5; max-width:420px;
}
/* Tabs */
.ls-tabs-list {
display: flex; gap: 0.5rem; margin-bottom: 1.25rem;
/* ── Search ── */
.ls-search-wrap {
position: relative;
animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) 0.08s both;
}
.ls-tab-btn {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.55rem 1.1rem; border-radius: 100px; cursor: pointer; border: none;
font-family: 'Nunito', sans-serif; font-size: 0.82rem; font-weight: 800;
transition: all 0.2s ease;
background: white; border: 2.5px solid #f3f4f6; color: #9ca3af;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.ls-tab-btn.active {
background: #1e1b4b; border-color: #1e1b4b; color: white;
box-shadow: 0 4px 0 #1e1b4b66;
}
.ls-tab-btn:not(.active):hover { border-color: #c4b5fd; color: #7c3aed; }
/* Lesson grid */
.ls-grid {
display: grid; gap: 0.85rem; grid-template-columns: 1fr;
}
@media(min-width: 480px) { .ls-grid { grid-template-columns: 1fr 1fr; } }
/* Lesson card */
.ls-card {
background: white; border: 2.5px solid #f3f4f6; border-radius: 22px;
overflow: hidden; cursor: pointer;
.ls-search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
border-radius: 16px;
border: 2.5px solid #f3f4f6;
background: white;
font-family: 'Nunito', sans-serif;
font-size: 0.875rem; font-weight: 700; color: #1e1b4b;
outline: none;
box-shadow: 0 4px 14px rgba(0,0,0,0.04);
transition: transform 0.15s ease, box-shadow 0.15s ease;
display: flex; flex-direction: column;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
box-sizing: border-box;
}
.ls-card:hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgba(0,0,0,0.08); }
.ls-card:active { transform: translateY(1px); box-shadow: 0 3px 8px rgba(0,0,0,0.05); }
.ls-search-input::placeholder { color: #c4b5fd; font-weight: 600; }
.ls-search-input:focus {
border-color: #a855f7;
box-shadow: 0 4px 20px rgba(168,85,247,0.15);
}
.ls-search-icon {
position: absolute; left: 0.875rem; top: 50%; transform: translateY(-50%);
color: #c4b5fd; pointer-events: none; transition: color 0.2s ease;
}
.ls-search-wrap:focus-within .ls-search-icon { color: #a855f7; }
.ls-search-clear {
position: absolute; right: 0.875rem; top: 50%; transform: translateY(-50%);
background: #f3f4f6; border: none; border-radius: 50%; cursor: pointer;
width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
color: #9ca3af; transition: background 0.15s ease, color 0.15s ease; padding: 0;
}
.ls-search-clear:hover { background: #f3e8ff; color: #a855f7; }
.ls-search-results-hint {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.72rem; font-weight: 700; color: #9ca3af;
padding: 0 0.25rem;
display: flex; align-items: center; gap: 0.4rem;
}
.ls-search-results-hint strong { color: #a855f7; }
.ls-card-thumb {
width: 100%; aspect-ratio: 16/9; object-fit: cover;
display: block; background: #f3f4f6;
/* ── Tabs ── */
.ls-tabs-list { display:flex; gap:0.5rem; }
.ls-tab-btn {
display:flex; align-items:center; gap:0.5rem;
padding:0.55rem 1.1rem; border-radius:100px; cursor:pointer; border:none;
font-family:'Nunito',sans-serif; font-size:0.82rem; font-weight:800;
transition:all 0.2s ease;
background:white; border:2.5px solid #f3f4f6; color:#9ca3af;
box-shadow:0 2px 8px rgba(0,0,0,0.04);
}
.ls-card-body { padding: 0.9rem 1rem 1rem; display: flex; flex-direction: column; gap: 0.25rem; flex: 1; }
.ls-card-title { font-size: 0.92rem; font-weight: 900; color: #1e1b4b; line-height: 1.3; }
.ls-card-topic {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase;
display: flex; align-items: center; gap: 0.35rem;
.ls-tab-btn.active { background:#1e1b4b; border-color:#1e1b4b; color:white; box-shadow:0 4px 0 #1e1b4b66; }
.ls-tab-btn:not(.active):hover { border-color:#c4b5fd; color:#7c3aed; }
/* ── Video sub-tabs ── */
.ls-video-subtabs {
display: flex; gap: 0.4rem;
padding: 0.5rem;
background: white; border: 2px solid #f3f4f6; border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
animation: lsPopIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both;
}
.ls-video-subtab-btn {
flex: 1; display: flex; align-items: center; justify-content: center; gap: 0.45rem;
padding: 0.5rem 1rem; border-radius: 10px; cursor: pointer; border: none;
font-family: 'Nunito', sans-serif; font-size: 0.78rem; font-weight: 800;
transition: all 0.2s ease; color: #9ca3af; background: transparent;
}
.ls-video-subtab-btn.active { background: #fff7ed; color: #f97316; box-shadow: 0 2px 8px rgba(249,115,22,0.15); }
.ls-video-subtab-btn:not(.active):hover { background: #fafafa; color: #6b7280; }
.ls-video-subtab-dot { width: 7px; height: 7px; border-radius: 50%; }
.ls-video-subtab-btn.active .ls-video-subtab-dot { background: #f97316; }
.ls-video-subtab-btn:not(.active) .ls-video-subtab-dot { background: #d1d5db; }
/* ── Section group ── */
.ls-group { display:flex; flex-direction:column; gap:0; }
.ls-group-header {
display:flex; align-items:center; gap:0.75rem;
padding:0.6rem 1rem; margin-bottom:0;
}
.ls-group-accent { width:3px; height:1.1rem; border-radius:100px; flex-shrink:0; }
.ls-group-accent.rw { background:#a855f7; }
.ls-group-accent.math { background:#0891b2; }
.ls-group-accent.video{ background:#f97316; }
.ls-group-name {
font-family:'Nunito Sans',sans-serif;
font-size:0.68rem; font-weight:700; letter-spacing:0.14em;
text-transform:uppercase; color:#9ca3af; flex:1;
}
.ls-group-count { font-family:'Nunito',sans-serif; font-size:0.68rem; font-weight:800; color:#d1d5db; }
/* ── Lesson list card ── */
.ls-list {
background:white; border:2px solid #f3f4f6; border-radius:20px;
overflow:hidden; box-shadow:0 4px 14px rgba(0,0,0,0.04);
animation:lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both;
}
.ls-lesson-row {
display:flex; align-items:center; gap:1rem;
padding:1rem 1.1rem; cursor:pointer;
transition:background 0.15s ease; position:relative;
}
.ls-lesson-row:not(:last-child)::after {
content:''; position:absolute; bottom:0; left:1.1rem; right:1.1rem;
height:1px; background:#f3f4f6;
}
.ls-lesson-row:hover { background:#fafafa; }
.ls-lesson-row:active { background:#f5f3ff; }
.ls-row-num {
font-family:'Nunito',sans-serif; font-size:0.7rem; font-weight:900; color:#d1d5db;
width:1.4rem; text-align:center; flex-shrink:0; letter-spacing:0.04em;
}
.ls-row-icon {
width:36px; height:36px; border-radius:10px; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
}
.ls-row-icon.rw { background:#f3e8ff; color:#a855f7; }
.ls-row-icon.math { background:#e0f2fe; color:#0891b2; }
.ls-row-icon.video { background:#fff7ed; color:#f97316; }
.ls-row-body { flex:1; min-width:0; }
.ls-row-title {
font-size:0.9rem; font-weight:800; color:#1e1b4b;
line-height:1.3; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.ls-row-desc {
font-family:'Nunito Sans',sans-serif;
font-size:0.72rem; font-weight:600; color:#9ca3af;
margin-top:0.15rem; line-height:1.4;
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.ls-status {
display:flex; align-items:center; gap:0.3rem;
padding:0.25rem 0.6rem; border-radius:100px; flex-shrink:0;
font-family:'Nunito',sans-serif; font-size:0.65rem; font-weight:800;
letter-spacing:0.06em; text-transform:uppercase; white-space:nowrap;
}
.ls-status.started { background:#fef9c3; color:#ca8a04; border:1.5px solid #fde047; }
.ls-status.completed { background:#dcfce7; color:#16a34a; border:1.5px solid #86efac; }
.ls-chevron { color:#d1d5db; flex-shrink:0; }
.ls-lesson-row:hover .ls-chevron { color:#a855f7; }
/* ── Search highlight ── */
.ls-highlight {
background: linear-gradient(120deg, #fde68a 0%, #fbbf24 100%);
border-radius: 3px; padding: 0 2px; color: #92400e;
}
/* ── Video Cards Grid ── */
.ls-video-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.875rem; }
@media (max-width: 480px) { .ls-video-grid { grid-template-columns: 1fr; } }
.ls-video-card {
background: white; border: 2px solid #f3f4f6; border-radius: 16px;
overflow: hidden; cursor: pointer;
box-shadow: 0 4px 14px rgba(0,0,0,0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both;
}
.ls-video-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(249,115,22,0.15); border-color: #fed7aa; }
.ls-video-card:active { transform: translateY(-1px); }
.ls-video-thumb {
position: relative; width: 100%; aspect-ratio: 16 / 9;
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%);
overflow: hidden;
}
.ls-video-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; }
.ls-video-card:hover .ls-video-thumb img { transform: scale(1.04); }
.ls-video-thumb-overlay {
position: absolute; inset: 0;
background: linear-gradient(to top, rgba(30,27,75,0.7) 0%, rgba(30,27,75,0.1) 50%, transparent 100%);
transition: opacity 0.2s ease;
}
.ls-video-card:hover .ls-video-thumb-overlay { opacity: 0.85; }
.ls-video-play-btn {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 40px; height: 40px; background: rgba(255,255,255,0.95); border-radius: 50%;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
transition: transform 0.2s ease, background 0.2s ease; color: #f97316;
}
.ls-video-card:hover .ls-video-play-btn { transform: translate(-50%, -50%) scale(1.1); background: white; }
.ls-video-thumb-fallback {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 60%, #4c1d95 100%);
font-size: 1.6rem; opacity: 0.7;
}
.ls-video-card-body { padding: 0.75rem 0.875rem 0.875rem; }
.ls-video-card-title {
font-size: 0.82rem; font-weight: 800; color: #1e1b4b; line-height: 1.35;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.ls-video-card-topic {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.68rem; font-weight: 600; color: #a855f7;
margin-top: 0.25rem;
}
.ls-card-topic.rw { color: #a855f7; }
.ls-card-topic.math { color: #0891b2; }
.ls-topic-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.ls-topic-dot.rw { background: #a855f7; }
.ls-topic-dot.math { background: #0891b2; }
/* Empty / error */
.ls-empty {
grid-column: 1 / -1; text-align: center; padding: 3rem 1rem;
background: white; border: 2.5px dashed #e5e7eb; border-radius: 22px;
display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
.ls-video-card-arrow {
color: #f97316; opacity: 0.5; margin-top: 0.5rem;
display: flex; justify-content: flex-end;
transition: opacity 0.15s ease, transform 0.15s ease;
}
.ls-empty-emoji { font-size: 2.5rem; }
.ls-empty-text { font-size: 0.9rem; font-weight: 700; color: #9ca3af; }
.ls-video-card:hover .ls-video-card-arrow { opacity: 1; transform: translateX(2px); }
/* Skeleton shimmer override */
.ls-skeleton-grid { display: grid; gap: 0.85rem; grid-template-columns: 1fr; }
@media(min-width: 480px) { .ls-skeleton-grid { grid-template-columns: 1fr 1fr; } }
/* ── Skeleton ── */
.ls-skel-list { background:white; border:2px solid #f3f4f6; border-radius:20px; overflow:hidden; box-shadow:0 4px 14px rgba(0,0,0,0.04); }
.ls-skel-row { display:flex; align-items:center; gap:1rem; padding:1rem 1.1rem; position:relative; }
.ls-skel-row:not(:last-child)::after { content:''; position:absolute; bottom:0; left:1.1rem; right:1.1rem; height:1px; background:#f3f4f6; }
.ls-skel-circle { width:36px; height:36px; border-radius:10px; flex-shrink:0; }
.ls-skel-lines { flex:1; display:flex; flex-direction:column; gap:0.4rem; }
.ls-skel-line { height:10px; border-radius:100px; }
.ls-skel-video-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.875rem; }
.ls-skel-video-card { background:white; border:2px solid #f3f4f6; border-radius:16px; overflow:hidden; }
.ls-skel-video-thumb { aspect-ratio:16/9; }
.ls-skel-video-body { padding:0.75rem; display:flex; flex-direction:column; gap:0.4rem; }
@keyframes lsSkelShimmer { 0%{ background-position:200% 0; } 100%{ background-position:-200% 0; } }
.ls-skel-circle,.ls-skel-line,.ls-skel-video-thumb,.ls-skel-video-body .ls-skel-line {
background:linear-gradient(90deg,#f3f4f6 25%,#e9eaec 50%,#f3f4f6 75%);
background-size:200% 100%; animation:lsSkelShimmer 1.4s ease-in-out infinite;
}
/* ── Empty ── */
.ls-empty {
text-align:center; padding:3rem 1rem;
background:white; border:2.5px dashed #e5e7eb; border-radius:20px;
display:flex; flex-direction:column; align-items:center; gap:0.5rem;
}
.ls-empty-emoji { font-size:2.5rem; }
.ls-empty-text { font-size:0.9rem; font-weight:700; color:#9ca3af; }
.ls-empty-sub { font-family:'Nunito Sans',sans-serif; font-size:0.78rem; color:#c4b5fd; font-weight:600; }
`;
// ─── Helpers ─────────────────────────────────────────────────────────────────
function highlightText(text: string, query: string): React.ReactNode {
if (!query.trim()) return text;
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx === -1) return text;
return (
<>
{text.slice(0, idx)}
<mark className="ls-highlight">
{text.slice(idx, idx + query.length)}
</mark>
{text.slice(idx + query.length)}
</>
);
}
function filterLessonMetadata(
lessons: LessonMetadata[],
query: string,
): LessonMetadata[] {
if (!query.trim()) return lessons;
const q = query.toLowerCase();
return lessons.filter(
(l) =>
l.title.toLowerCase().includes(q) ||
l.description.toLowerCase().includes(q) ||
l.category.toLowerCase().includes(q),
);
}
function filterVideoLessons(lessons: Lesson[], query: string): Lesson[] {
if (!query.trim()) return lessons;
const q = query.toLowerCase();
return lessons.filter(
(l) =>
l.title.toLowerCase().includes(q) ||
l.topic?.name?.toLowerCase().includes(q),
);
}
// ─── Skeletons ────────────────────────────────────────────────────────────────
const SkeletonGroup = () => (
<div className="ls-group">
<div className="ls-skel-list">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="ls-skel-row">
<div className="ls-skel-circle" />
<div className="ls-skel-lines">
<div
className="ls-skel-line"
style={{ width: `${60 + ((i * 11) % 30)}%` }}
/>
<div
className="ls-skel-line"
style={{ width: `${40 + ((i * 7) % 35)}%` }}
/>
</div>
</div>
))}
</div>
</div>
);
const SkeletonVideoGrid = () => (
<div className="ls-skel-video-grid">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="ls-skel-video-card">
<div className="ls-skel-video-thumb" />
<div className="ls-skel-video-body">
<div className="ls-skel-line" style={{ width: "80%" }} />
<div className="ls-skel-line" style={{ width: "55%" }} />
</div>
</div>
))}
</div>
);
// ─── Video Card ───────────────────────────────────────────────────────────────
interface VideoCardProps {
lesson: Lesson; // uses the Lesson type from API: id, title, thumbnail_url, topic
index: number;
searchQuery: string;
onClick: () => void;
}
const VideoCard = ({ lesson, index, searchQuery, onClick }: VideoCardProps) => (
<div
className="ls-video-card"
style={{ animationDelay: `${0.05 + index * 0.05}s` }}
onClick={onClick}
>
<div className="ls-video-thumb">
{lesson.thumbnail_url ? (
<>
<img src={lesson.thumbnail_url} alt={lesson.title} loading="lazy" />
<div className="ls-video-thumb-overlay" />
</>
) : (
<div className="ls-video-thumb-fallback">🎬</div>
)}
<div className="ls-video-play-btn">
<Play size={16} fill="currentColor" strokeWidth={0} />
</div>
</div>
<div className="ls-video-card-body">
<p className="ls-video-card-title">
{highlightText(lesson.title, searchQuery)}
</p>
{lesson.topic?.name && (
<p className="ls-video-card-topic">
{highlightText(lesson.topic.name, searchQuery)}
</p>
)}
<div className="ls-video-card-arrow">
<ChevronRight size={14} />
</div>
</div>
</div>
);
// ─── Component ────────────────────────────────────────────────────────────────
export const Lessons = () => {
const user = useAuthStore((state) => state.user);
const [lessons, setLessons] = useState<Lesson[]>([]);
const user = useAuthStore((s) => s.user);
// Video lessons from API — typed as Lesson[]
const [allVideos, setAllVideos] = useState<Lesson[]>([]);
const [lessonLoading, setLessonLoading] = useState(true);
const [activeTab, setActiveTab] = useState<"rw" | "math">("rw");
const [activeTab, setActiveTab] = useState<"rw" | "math" | "video">("rw");
const [videoSubTab, setVideoSubTab] = useState<VideoSubTab>("rw");
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const handleLessonClick = (id: string) => {
setSelectedLessonId(id);
@ -153,7 +463,7 @@ export const Lessons = () => {
};
useEffect(() => {
const fetchAllLessons = async () => {
const fetchVideos = async () => {
if (!user) return;
try {
setLessonLoading(true);
@ -163,74 +473,176 @@ export const Lessons = () => {
state: { token },
} = JSON.parse(authStorage) as { state?: { token?: string } };
if (!token) return;
const response = await api.fetchAllLessons(token);
setLessons(response.data);
const response = await api.fetchLessonVideos(token);
// response matches LessonsResponse: { data: Lesson[], pagination: ... }
setAllVideos(response.data);
} catch (e) {
console.error(e);
} finally {
setLessonLoading(false);
}
};
fetchAllLessons();
fetchVideos();
}, [user]);
const renderGrid = (variant: "rw" | "math") => {
if (lessonLoading) {
return (
<div className="ls-skeleton-grid">
{Array.from({ length: 6 }).map((_, i) => (
<LessonSkeleton key={i} />
))}
</div>
// Split videos by sub-tab using topic name (the only subject signal in Lesson type)
const videos = useMemo<Lesson[]>(() => {
if (videoSubTab === "math") {
return allVideos.filter((v) =>
v.topic?.name?.toLowerCase().includes("math"),
);
}
return allVideos.filter(
(v) => !v.topic?.name?.toLowerCase().includes("math"),
);
}, [allVideos, videoSubTab]);
// Count for search hint
const totalCount = useMemo(() => {
if (activeTab === "video") return videos.length;
return activeTab === "math" ? MATH_LESSONS.length : EBRW_LESSONS.length;
}, [activeTab, videos.length]);
const filteredCount = useMemo(() => {
if (activeTab === "video")
return filterVideoLessons(videos, searchQuery).length;
const base = activeTab === "math" ? MATH_LESSONS : EBRW_LESSONS;
return filterLessonMetadata(base, searchQuery).length;
}, [activeTab, videos, searchQuery]);
// ── Local lesson group renderer (LessonMetadata[]) ──
const renderLessonGroups = (rawLessons: LessonMetadata[]) => {
const lessons = filterLessonMetadata(rawLessons, searchQuery);
if (!lessons.length) {
return (
<div className="ls-grid">
<div className="ls-empty">
<span className="ls-empty-emoji">📭</span>
<p className="ls-empty-text">No lessons available yet.</p>
</div>
<div className="ls-empty">
<span className="ls-empty-emoji">🔍</span>
<p className="ls-empty-text">No lessons found for "{searchQuery}"</p>
<p className="ls-empty-sub">Try a different search term</p>
</div>
);
}
return (
<div className="ls-grid">
{lessons.map((lesson) => (
<div
key={lesson.id}
className="ls-card"
onClick={() => handleLessonClick(lesson.id)}
>
<img
src={lesson.thumbnail_url}
alt={lesson.title}
className="ls-card-thumb"
/>
<div className="ls-card-body">
<p className="ls-card-title">{lesson.title}</p>
<p className={`ls-card-topic ${variant}`}>
<span className={`ls-topic-dot ${variant}`} />
{lesson.topic.name}
</p>
const grouped = lessons.reduce<Record<string, LessonMetadata[]>>(
(acc, lesson) => {
if (!acc[lesson.category]) acc[lesson.category] = [];
acc[lesson.category].push(lesson);
return acc;
},
{},
);
return Object.entries(grouped).map(([category, categoryLessons], gi) => (
<div
key={category}
className="ls-group ls-anim"
style={{ animationDelay: `${0.05 + gi * 0.06}s` }}
>
<div className="ls-group-header">
<div className={`ls-group-accent ${categoryLessons[0].color}`} />
<span className="ls-group-name">
{highlightText(category, searchQuery)}
</span>
<span className="ls-group-count">{categoryLessons.length}</span>
</div>
<div className="ls-list">
{categoryLessons.map((lesson, li) => (
<div
key={lesson.id}
className="ls-lesson-row"
onClick={() => handleLessonClick(lesson.id)}
>
<span className="ls-row-num">
{String(li + 1).padStart(2, "0")}
</span>
<div className={`ls-row-icon ${lesson.color}`}>
{renderLessonIcon(lesson.iconName)}
</div>
<div className="ls-row-body">
<p className="ls-row-title">
{highlightText(lesson.title, searchQuery)}
</p>
{lesson.description && (
<p className="ls-row-desc">
{highlightText(lesson.description, searchQuery)}
</p>
)}
</div>
<ChevronRight size={16} className="ls-chevron" />
</div>
</div>
))}
</div>
</div>
));
};
// ── Video grid renderer (Lesson[]) ──
const renderVideoGrid = () => {
const filtered = filterVideoLessons(videos, searchQuery);
if (!filtered.length) {
return (
<div className="ls-empty">
<span className="ls-empty-emoji">{searchQuery ? "🔍" : "📭"}</span>
<p className="ls-empty-text">
{searchQuery
? `No videos found for "${searchQuery}"`
: "No videos available yet."}
</p>
{searchQuery && (
<p className="ls-empty-sub">Try a different search term</p>
)}
</div>
);
}
return (
<div className="ls-video-grid">
{filtered.map((lesson, i) => (
<VideoCard
key={lesson.id}
lesson={lesson}
index={i}
searchQuery={searchQuery}
onClick={() => handleLessonClick(lesson.id)}
/>
))}
</div>
);
};
const renderContent = () => {
if (lessonLoading) {
return activeTab === "video" ? (
<SkeletonVideoGrid />
) : (
<>
<SkeletonGroup />
<SkeletonGroup />
</>
);
}
if (activeTab === "video") return renderVideoGrid();
if (activeTab === "math") return renderLessonGroups(MATH_LESSONS);
return renderLessonGroups(EBRW_LESSONS);
};
const isSearchActive = searchQuery.trim().length > 0;
return (
<div className="ls-screen pb-12">
<style>{STYLES}</style>
{/* Blobs */}
<div className="ls-blob ls-blob-1" />
<div className="ls-blob ls-blob-2" />
<div className="ls-blob ls-blob-3" />
<div className="ls-blob ls-blob-4" />
{/* Dots */}
{DOTS.map((d, i) => (
<div
key={i}
@ -241,8 +653,8 @@ export const Lessons = () => {
height: d.size,
background: d.color,
top: d.top,
left: d.left,
right: d.right,
left: (d as any).left,
right: (d as any).right,
animationDelay: d.delay,
animationDuration: `${5 + i * 0.5}s`,
} as React.CSSProperties
@ -251,7 +663,6 @@ export const Lessons = () => {
))}
<div className="ls-inner">
{/* Header */}
<header className="ls-header">
<h1 className="ls-title">📚 Lessons</h1>
<p className="ls-sub">
@ -260,14 +671,43 @@ export const Lessons = () => {
</p>
</header>
{/* Tabs + content */}
{/* Search */}
<div className="ls-search-wrap">
<Search size={16} className="ls-search-icon" />
<input
className="ls-search-input"
type="text"
placeholder="Search lessons, topics, categories…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{isSearchActive && (
<button
className="ls-search-clear"
onClick={() => setSearchQuery("")}
>
<X size={12} />
</button>
)}
</div>
{isSearchActive && !lessonLoading && (
<div className="ls-search-results-hint">
<Search size={11} />
<span>
<strong>{filteredCount}</strong> of {totalCount} lessons match "
<strong>{searchQuery}</strong>"
</span>
</div>
)}
<section className="ls-anim ls-anim-1">
<div className="ls-tabs-list">
<div className="ls-tabs-list" style={{ marginBottom: "1.25rem" }}>
<button
className={`ls-tab-btn${activeTab === "rw" ? " active" : ""}`}
onClick={() => setActiveTab("rw")}
>
<BookOpen size={15} /> Reading & Writing
<BookOpen size={15} /> {truncate("Reading & Writing")}
</button>
<button
className={`ls-tab-btn${activeTab === "math" ? " active" : ""}`}
@ -275,9 +715,37 @@ export const Lessons = () => {
>
<Calculator size={15} /> Math
</button>
<button
className={`ls-tab-btn${activeTab === "video" ? " active" : ""}`}
onClick={() => setActiveTab("video")}
>
<Video size={15} /> Videos
</button>
</div>
{renderGrid(activeTab)}
{activeTab === "video" && (
<div
className="ls-video-subtabs"
style={{ marginBottom: "1.25rem" }}
>
<button
className={`ls-video-subtab-btn${videoSubTab === "rw" ? " active" : ""}`}
onClick={() => setVideoSubTab("rw")}
>
<span className="ls-video-subtab-dot" />
<BookOpen size={13} /> Reading &amp; Writing
</button>
<button
className={`ls-video-subtab-btn${videoSubTab === "math" ? " active" : ""}`}
onClick={() => setVideoSubTab("math")}
>
<span className="ls-video-subtab-dot" />
<Calculator size={13} /> Math
</button>
</div>
)}
{renderContent()}
</section>
</div>

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

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

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

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

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

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

View 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 &minus; 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 &minus; 1.5 &times; IQR
</p>
<p className="text-red-700 font-bold">
Upper Bound: Q3 + 1.5 &times; 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 &gt; Median &gt; 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 &lt; Median &lt; 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?" &rarr; Compare IQR or SD
(larger value = more spread out).
<br />
"Which group has a higher typical value?" &rarr; Compare medians
or means.
<br />
"Are the distributions similar in shape?" &rarr; 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;

View 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: "SubjectVerb Comma",
desc: "Never put a single comma between a subject and its verb.",
},
].map((t) => (
<div
key={t.label}
className="flex gap-3 bg-red-50 border border-red-200 rounded-xl px-4 py-3"
>
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-red-800">{t.label}</p>
<p className="text-xs text-slate-600 mt-0.5">{t.desc}</p>
</div>
</div>
))}
</div>
<DecisionTreeWidget scenarios={TREE_SCENARIOS} accentColor="purple" />
<button
onClick={() => scrollToSection(2)}
className="mt-12 group flex items-center text-purple-600 font-bold hover:text-purple-800 transition-colors"
>
Next: Practice Questions{" "}
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
</button>
</section>
{/* Section 2 — Quiz */}
<section
ref={(el) => {
sectionsRef.current[2] = el;
}}
className="min-h-screen flex flex-col justify-center mb-24"
>
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
Practice Questions
</h2>
{BOUNDARIES_EASY.slice(0, 2).map((q) => (
<PracticeFromDataset key={q.id} question={q} color="purple" />
))}
{BOUNDARIES_MEDIUM.slice(0, 1).map((q) => (
<PracticeFromDataset key={q.id} question={q} color="purple" />
))}
<div className="mt-8 text-center">
<button
onClick={onFinish}
className="px-6 py-3 bg-purple-900 text-white font-bold rounded-full hover:bg-purple-700 transition-colors"
>
Finish Lesson
</button>
</div>
</section>
</div>
</div>
);
};
export default EBRWCommasLesson;

View 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 &amp; Structure
</div>
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
Craft &amp; 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;

View 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 &amp; 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;

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

View 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 &amp; 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;

View 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 911, 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 19511980 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 1317",
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 1824",
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 1317) 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 1317, exceeding Streaming (3.2 hr), Video Games (2.5 hr), and Reading (0.7 hr). Directly supported.",
},
{
text: "Adults ages 1824 spend more time on streaming video than teenagers do.",
verdict: "supported",
explanation:
"Ages 1824: Streaming = 3.9 hr. Ages 1317: 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 (20102022), 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 (20002020)"
</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;

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

View 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, 13°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 13 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 14 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 (12 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">
12 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;

View 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 &amp; 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;

View 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 &amp; 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;

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

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

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

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

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

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

View 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 &nbsp;&nbsp;and&nbsp;&nbsp; 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>ab</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>
);
}

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

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

View 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) &nbsp;|&nbsp;{" "}
<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 &nbsp;|&nbsp; 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>
);
}

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

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

View 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">&lt;</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">&gt;</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 &gt; 16</p>
<p className="text-slate-500">3x &gt; 9</p>
<p className="text-slate-500">
Divide by 3 <strong className="text-red-600">FLIP</strong> x
&lt; 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 &lt; 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 &lt; 3 OR x &gt; 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 &lt; 3x + 1 10</p>
<p className="text-slate-500">Subtract 1: 3 &lt; 3x 9</p>
<p className="text-slate-500">
Divide by 3:{" "}
<strong className="text-blue-700">1 &lt; x 3</strong>
</p>
</ExampleCard>
<div className="mt-4">
<ExampleCard title="Example: OR" color="blue">
<p>Solve: x + 1 &lt; 2 OR x + 1 &gt; 4</p>
<p className="text-slate-500">x &lt; 3 OR x &gt; 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">
&lt; or &gt;
</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 &lt; or &gt; (not
included)
</p>
<p>
3. Use a <strong>solid line</strong> for or (included)
</p>
<p>
4. Shade <strong>above</strong> for y &gt; or y and{" "}
<strong>below</strong> for y &lt; 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 &gt; 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>
);
}

View 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 &nbsp; 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;

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

View 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 &gt; c, a + c &gt; b, b + c &gt; a</p>
<p className="mt-1">
Example: Can a triangle have sides 3, 5, 9?
</p>
<p>3 + 5 = 8 &lt; 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 = 52</p>
<p>
If hyp = 8: leg = <Frac n="8" d="√2" /> = 42
</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 = 43, hyp = 8</p>
<p>Hyp = 10: short leg = 5, long leg = 53</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) = 53 {" "}
<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;

View 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 &gt;
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 : x2</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 : x3 : 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">72 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">53 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>
);
}

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

View 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 &gt; 0 opens up (minimum). If a &lt; 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> &nbsp; where b = 1 + r &gt; 1
</FormulaBox>
<FormulaBox>
Decay: f(x) = a(1 r)<sup>x</sup> &nbsp; where b = 1 r &lt; 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 &gt; 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 &lt; b &lt; 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 &gt; 1) or compress (0 &lt; a &lt; 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>
);
}

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

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

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

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

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

View 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 &gt; 0</strong>: parabola opens upward (U-shape);{" "}
<strong>a &lt; 0</strong>: opens downward (-shape)
</li>
<li>
<strong>|a| &gt; 1</strong>: narrow parabola;{" "}
<strong>|a| &lt; 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 &gt; 0) or maximum value (if a
&lt; 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 &lt; 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">Δ &gt; 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">Δ &lt; 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² + (bm)x + (cb). 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">
Δ &gt; 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;

View 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 &lt; 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 &gt; 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;

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

View 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" /> &nbsp;means&nbsp; 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>
);
}

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

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

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

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

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

View 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"
>
x2 {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"
>
x3 {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 = x2.
</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 = x3, 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;

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