feat(lessons): add new lessons for english section
This commit is contained in:
@ -21,7 +21,6 @@ import { Drills } from "./pages/student/drills/page";
|
||||
import { HardTestModules } from "./pages/student/hard-test-modules/page";
|
||||
import { Analytics } from "./pages/student/Analytics";
|
||||
import { QuestMap } from "./pages/student/QuestMap";
|
||||
import ErrorPage from "./pages/ErrorPage";
|
||||
import { Register } from "./pages/auth/Register";
|
||||
|
||||
function App() {
|
||||
@ -40,6 +39,7 @@ function App() {
|
||||
children: [
|
||||
{
|
||||
element: <StudentLayout />,
|
||||
|
||||
children: [
|
||||
{
|
||||
path: "home",
|
||||
|
||||
@ -5,22 +5,16 @@ import { lazy, type ComponentType } from "react";
|
||||
|
||||
export type LessonId =
|
||||
// ---- EBRW ----
|
||||
| "ebrw-main-idea"
|
||||
| "ebrw-explicit-meaning"
|
||||
| "ebrw-words-in-context"
|
||||
| "ebrw-text-structure-purpose"
|
||||
| "ebrw-cross-text-connections"
|
||||
| "ebrw-central-ideas-details"
|
||||
| "ebrw-inferences"
|
||||
| "ebrw-graphic-displays"
|
||||
| "ebrw-craft-structure"
|
||||
| "ebrw-vocab-precise"
|
||||
| "ebrw-vocab-meaning"
|
||||
| "ebrw-expression-ideas"
|
||||
| "ebrw-command-of-evidence"
|
||||
| "ebrw-boundaries"
|
||||
| "ebrw-form-structure-sense"
|
||||
| "ebrw-transitions"
|
||||
| "ebrw-commas"
|
||||
| "ebrw-semicolons-colons"
|
||||
| "ebrw-dashes-apostrophes"
|
||||
| "ebrw-subject-verb"
|
||||
| "ebrw-pronouns"
|
||||
| "ebrw-verbs"
|
||||
| "ebrw-sentence-structure"
|
||||
| "ebrw-rhetorical-synthesis"
|
||||
|
||||
// ---- MATH ----
|
||||
| "alg-linear-eq-1var"
|
||||
@ -45,55 +39,45 @@ export type LessonId =
|
||||
| "geom-circles";
|
||||
|
||||
// ---- EBRW ----
|
||||
const EBRWMainIdea = lazy(
|
||||
() => import("../pages/student/lessons/EBRWMainIdeaLesson"),
|
||||
const EBRWWordsInContext = lazy(
|
||||
() => import("../pages/student/lessons/EBRWWordsInContextLesson"),
|
||||
);
|
||||
const EBRWExplicitMeaning = lazy(
|
||||
() => import("../pages/student/lessons/EBRWExplicitMeaningLesson"),
|
||||
|
||||
const EBRWTextStructurePurpose = lazy(
|
||||
() => import("../pages/student/lessons/EBRWTextStructurePurposeLesson"),
|
||||
);
|
||||
|
||||
const EBRWCrossText = lazy(
|
||||
() => import("../pages/student/lessons/EBRWCrossTextLesson"),
|
||||
);
|
||||
|
||||
const EBRWCentralIdeas = lazy(
|
||||
() => import("../pages/student/lessons/EBRWCentralIdeasLesson"),
|
||||
);
|
||||
|
||||
const EBRWInferences = lazy(
|
||||
() => import("../pages/student/lessons/EBRWInferencesLesson"),
|
||||
);
|
||||
const EBRWGraphicDisplays = lazy(
|
||||
() => import("../pages/student/lessons/EBRWGraphicDisplaysLesson"),
|
||||
|
||||
const EBRWCommandEvidence = lazy(
|
||||
() => import("../pages/student/lessons/EBRWCommandEvidenceLesson"),
|
||||
);
|
||||
const EBRWCraftStructure = lazy(
|
||||
() => import("../pages/student/lessons/EBRWCraftStructureLesson"),
|
||||
|
||||
const EBRWBoundaries = lazy(
|
||||
() => import("../pages/student/lessons/EBRWBoundariesLesson"),
|
||||
);
|
||||
const EBRWVocabPrecise = lazy(
|
||||
() => import("../pages/student/lessons/EBRWVocabPreciseLesson"),
|
||||
);
|
||||
const EBRWVocabMeaning = lazy(
|
||||
() => import("../pages/student/lessons/EBRWVocabMeaningLesson"),
|
||||
);
|
||||
const EBRWExpressionIdeas = lazy(
|
||||
() => import("../pages/student/lessons/EBRWExpressionIdeasLesson"),
|
||||
|
||||
const EBRWFormStructureSense = lazy(
|
||||
() => import("../pages/student/lessons/EBRWFormStructureSenseLesson"),
|
||||
);
|
||||
|
||||
const EBRWTransitions = lazy(
|
||||
() => import("../pages/student/lessons/EBRWTransitionsLesson"),
|
||||
);
|
||||
const EBRWCommas = lazy(
|
||||
() => import("../pages/student/lessons/EBRWCommasLesson"),
|
||||
);
|
||||
const EBRWSemicolonsColons = lazy(
|
||||
() => import("../pages/student/lessons/EBRWSemicolonsColonsLesson"),
|
||||
);
|
||||
const EBRWDashesApostrophes = lazy(
|
||||
() => import("../pages/student/lessons/EBRWDashesApostrophesLesson"),
|
||||
);
|
||||
const EBRWSubjectVerb = lazy(
|
||||
() => import("../pages/student/lessons/EBRWSubjectVerbLesson"),
|
||||
);
|
||||
const EBRWPronouns = lazy(
|
||||
() => import("../pages/student/lessons/EBRWPronounsLesson"),
|
||||
);
|
||||
const EBRWVerbs = lazy(
|
||||
() => import("../pages/student/lessons/EBRWVerbsLesson"),
|
||||
);
|
||||
const EBRWSentenceStructure = lazy(
|
||||
() => import("../pages/student/lessons/EBRWSentenceStructureLesson"),
|
||||
);
|
||||
|
||||
const EBRWRhetoricalSynthesis = lazy(
|
||||
() => import("../pages/student/lessons/EBRWRhetoricalSynthesisLesson"),
|
||||
);
|
||||
// ---- MATH ----
|
||||
const AlgLinearEq1Var = lazy(
|
||||
() => import("../pages/student/lessons/LinearEq1VarLesson"),
|
||||
@ -158,24 +142,19 @@ const GeomCircles = lazy(
|
||||
|
||||
// ---- Registry Map ----
|
||||
export const LESSON_COMPONENT_MAP: Record<LessonId, ComponentType> = {
|
||||
// EBRW
|
||||
"ebrw-main-idea": EBRWMainIdea,
|
||||
"ebrw-explicit-meaning": EBRWExplicitMeaning,
|
||||
// ---- EBRW ----
|
||||
"ebrw-words-in-context": EBRWWordsInContext,
|
||||
"ebrw-text-structure-purpose": EBRWTextStructurePurpose,
|
||||
"ebrw-cross-text-connections": EBRWCrossText,
|
||||
"ebrw-central-ideas-details": EBRWCentralIdeas,
|
||||
"ebrw-inferences": EBRWInferences,
|
||||
"ebrw-graphic-displays": EBRWGraphicDisplays,
|
||||
"ebrw-craft-structure": EBRWCraftStructure,
|
||||
"ebrw-vocab-precise": EBRWVocabPrecise,
|
||||
"ebrw-vocab-meaning": EBRWVocabMeaning,
|
||||
"ebrw-expression-ideas": EBRWExpressionIdeas,
|
||||
"ebrw-command-of-evidence": EBRWCommandEvidence,
|
||||
"ebrw-boundaries": EBRWBoundaries,
|
||||
"ebrw-form-structure-sense": EBRWFormStructureSense,
|
||||
"ebrw-transitions": EBRWTransitions,
|
||||
"ebrw-commas": EBRWCommas,
|
||||
"ebrw-semicolons-colons": EBRWSemicolonsColons,
|
||||
"ebrw-dashes-apostrophes": EBRWDashesApostrophes,
|
||||
"ebrw-subject-verb": EBRWSubjectVerb,
|
||||
"ebrw-pronouns": EBRWPronouns,
|
||||
"ebrw-verbs": EBRWVerbs,
|
||||
"ebrw-sentence-structure": EBRWSentenceStructure,
|
||||
// MATH
|
||||
"ebrw-rhetorical-synthesis": EBRWRhetoricalSynthesis,
|
||||
|
||||
// ---- MATH ----
|
||||
"alg-linear-eq-1var": AlgLinearEq1Var,
|
||||
"alg-linear-eq-2var": AlgLinearEq2Var,
|
||||
"alg-linear-functions": AlgLinearFunctions,
|
||||
|
||||
@ -109,7 +109,7 @@ export default function LessonShell({
|
||||
const childArray = React.Children.toArray(children);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen lesson-bg">
|
||||
<div className="flex flex-col lg:flex-row min-h-screen">
|
||||
{/* ── Mobile toggle ── */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
@ -180,7 +180,7 @@ export default function LessonShell({
|
||||
</aside>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<div className="flex-1 lg:ml-64 max-w-4xl mx-auto w-full">
|
||||
<div className="flex-1 lg:ml-64 mx-auto md:p-12 max-w-full">
|
||||
{childArray.map((child, i) => (
|
||||
<section
|
||||
key={i}
|
||||
|
||||
114
src/components/lessons/RevealCardGrid.tsx
Normal file
114
src/components/lessons/RevealCardGrid.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export interface RevealCard {
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const COLORS: Record<string, {
|
||||
text: string; activeBg: string; activeBorder: string;
|
||||
hoverBorder: string; btn: string;
|
||||
}> = {
|
||||
fuchsia: { text: 'text-fuchsia-700', activeBg: 'bg-fuchsia-50', activeBorder: 'border-fuchsia-300', hoverBorder: 'hover:border-fuchsia-300', btn: 'text-fuchsia-600' },
|
||||
teal: { text: 'text-teal-700', activeBg: 'bg-teal-50', activeBorder: 'border-teal-300', hoverBorder: 'hover:border-teal-300', btn: 'text-teal-600' },
|
||||
purple: { text: 'text-purple-700', activeBg: 'bg-purple-50', activeBorder: 'border-purple-300', hoverBorder: 'hover:border-purple-300', btn: 'text-purple-600' },
|
||||
rose: { text: 'text-rose-700', activeBg: 'bg-rose-50', activeBorder: 'border-rose-300', hoverBorder: 'hover:border-rose-300', btn: 'text-rose-600' },
|
||||
indigo: { text: 'text-indigo-700', activeBg: 'bg-indigo-50', activeBorder: 'border-indigo-300', hoverBorder: 'hover:border-indigo-300', btn: 'text-indigo-600' },
|
||||
amber: { text: 'text-amber-700', activeBg: 'bg-amber-50', activeBorder: 'border-amber-300', hoverBorder: 'hover:border-amber-300', btn: 'text-amber-600' },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
cards: RevealCard[];
|
||||
columns?: 2 | 3 | 4 | 5;
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
const RevealCardGrid: React.FC<Props> = ({ cards, columns = 3, accentColor = 'fuchsia' }) => {
|
||||
const [revealed, setRevealed] = useState<Set<number>>(new Set());
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [entered, setEntered] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const c = COLORS[accentColor] || COLORS.fuchsia;
|
||||
|
||||
/* Entrance trigger — animate cards when grid scrolls into view */
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const obs = new IntersectionObserver(
|
||||
entries => { if (entries[0]?.isIntersecting) { setVisible(true); obs.disconnect(); } },
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
/* Clear stagger delays after entrance finishes so tap interactions are instant */
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const t = setTimeout(() => setEntered(true), Math.min(cards.length * 50, 600) + 400);
|
||||
return () => clearTimeout(t);
|
||||
}, [visible, cards.length]);
|
||||
|
||||
const toggle = (i: number) =>
|
||||
setRevealed(prev => { const s = new Set(prev); s.has(i) ? s.delete(i) : s.add(i); return s; });
|
||||
|
||||
const allDone = revealed.size === cards.length;
|
||||
|
||||
const colCls =
|
||||
columns === 2 ? 'grid-cols-1 sm:grid-cols-2'
|
||||
: columns === 4 ? 'grid-cols-2 sm:grid-cols-4'
|
||||
: columns === 5 ? 'grid-cols-2 sm:grid-cols-5'
|
||||
: 'grid-cols-2 sm:grid-cols-3';
|
||||
|
||||
return (
|
||||
<div ref={ref} className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-400 font-medium">
|
||||
{revealed.size}/{cards.length} revealed
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setRevealed(allDone ? new Set() : new Set(cards.map((_, i) => i)))}
|
||||
className={`text-xs font-bold ${c.btn} hover:underline transition-colors`}
|
||||
>
|
||||
{allDone ? 'Hide All' : 'Reveal All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`grid ${colCls} gap-2`}>
|
||||
{cards.map((card, i) => {
|
||||
const open = revealed.has(i);
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => toggle(i)}
|
||||
aria-expanded={open}
|
||||
className={[
|
||||
'text-left rounded-xl p-3 border transition-all duration-300 ease-out',
|
||||
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-3',
|
||||
open
|
||||
? `${c.activeBg} ${c.activeBorder} shadow-sm`
|
||||
: `bg-slate-50 border-slate-200 ${c.hoverBorder} hover:shadow-sm`,
|
||||
].join(' ')}
|
||||
style={{ transitionDelay: !entered ? `${Math.min(i * 50, 600)}ms` : '0ms' }}
|
||||
>
|
||||
<p className={`font-bold ${c.text} text-xs mb-0.5`}>{card.label}</p>
|
||||
{card.sublabel && (
|
||||
<p className="text-[10px] text-slate-500 font-medium mb-1">{card.sublabel}</p>
|
||||
)}
|
||||
<div className={`transition-opacity duration-200 ${open ? 'opacity-100' : 'opacity-50'}`}>
|
||||
{open ? (
|
||||
<p className="text-xs text-slate-600 leading-relaxed">{card.content}</p>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400 italic">tap to reveal</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RevealCardGrid;
|
||||
36
src/components/lessons/useScrollReveal.ts
Normal file
36
src/components/lessons/useScrollReveal.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Observes all `.scroll-reveal` elements in the DOM and adds the `revealed`
|
||||
* class when they scroll into view. Works with the stagger-1 … stagger-10
|
||||
* utility classes defined in index.css for sequenced entrance animations.
|
||||
*
|
||||
* Call once at the top of a lesson component:
|
||||
* useScrollReveal();
|
||||
*/
|
||||
export default function useScrollReveal() {
|
||||
useEffect(() => {
|
||||
const sel = [
|
||||
'.scroll-reveal:not(.revealed)',
|
||||
'.scroll-reveal-left:not(.revealed)',
|
||||
'.scroll-reveal-right:not(.revealed)',
|
||||
'.scroll-reveal-scale:not(.revealed)',
|
||||
].join(',');
|
||||
const els = document.querySelectorAll(sel);
|
||||
if (!els.length) return;
|
||||
|
||||
const obs = new IntersectionObserver(
|
||||
entries =>
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('revealed');
|
||||
obs.unobserve(e.target);
|
||||
}
|
||||
}),
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -60px 0px' },
|
||||
);
|
||||
|
||||
els.forEach(el => obs.observe(el));
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PracticeQuestion } from "../../types/lesson";
|
||||
import { type PracticeQuestion } from "../../types/lesson";
|
||||
|
||||
export const CENTRAL_IDEAS_EASY: PracticeQuestion[] = [
|
||||
{
|
||||
|
||||
@ -151,8 +151,8 @@ export const RW_TOPICS: TopicRegistry = {
|
||||
id: "transitions",
|
||||
name: "Transitions",
|
||||
section: "rw",
|
||||
category: "Standard English",
|
||||
color: "purple",
|
||||
category: "Expression of Ideas",
|
||||
color: "rose",
|
||||
questions: {
|
||||
easy: TRANSITIONS_EASY,
|
||||
medium: TRANSITIONS_MEDIUM,
|
||||
|
||||
@ -2,191 +2,363 @@ import { useState, useEffect } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import { Loader2, Mail, Lock } from "lucide-react";
|
||||
import { Loader2, Mail, Lock, Target, Clock, BarChart2 } from "lucide-react";
|
||||
|
||||
interface LocationState {
|
||||
from?: { pathname: string };
|
||||
}
|
||||
|
||||
const DOTS = [
|
||||
{ size: 12, color: "#f97316", top: "8%", left: "6%", delay: "0s" },
|
||||
{ size: 7, color: "#a855f7", top: "22%", left: "3%", delay: "1.2s" },
|
||||
{ size: 9, color: "#22c55e", top: "65%", left: "5%", delay: "0.6s" },
|
||||
{ size: 8, color: "#f43f5e", top: "80%", left: "8%", delay: "2.1s" },
|
||||
{ size: 12, color: "#3b82f6", top: "10%", right: "6%", delay: "1.8s" },
|
||||
{ size: 7, color: "#eab308", top: "40%", right: "3%", delay: "0.9s" },
|
||||
{ size: 10, color: "#a855f7", top: "72%", right: "5%", delay: "0.4s" },
|
||||
{ size: 8, color: "#f97316", top: "55%", right: "8%", delay: "1.5s" },
|
||||
];
|
||||
|
||||
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');
|
||||
|
||||
.lg-screen {
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
.lg-root {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
display: flex;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
background: #fffbf4;
|
||||
}
|
||||
|
||||
/* ─── LEFT PANEL ─── */
|
||||
.lg-left {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(160deg, #060d1f 0%, #0f2044 40%, #0e3476 75%, #1a56c4 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 3rem 3.5rem;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Animated grid */
|
||||
.lg-grid {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(99,179,255,0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99,179,255,0.06) 1px, transparent 1px);
|
||||
background-size: 52px 52px;
|
||||
animation: gridScroll 25s linear infinite;
|
||||
}
|
||||
@keyframes gridScroll {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 52px 52px; }
|
||||
}
|
||||
|
||||
/* Radial glow spots */
|
||||
.lg-glow {
|
||||
position: absolute; pointer-events: none; border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
}
|
||||
.lg-glow-1 { width: 380px; height: 380px; background: #1d4ed8; opacity: 0.35; top: -120px; right: -80px; animation: glowPulse 8s ease-in-out infinite; }
|
||||
.lg-glow-2 { width: 280px; height: 280px; background: #0ea5e9; opacity: 0.2; bottom: -60px; left: -60px; animation: glowPulse 10s ease-in-out infinite 2s; }
|
||||
.lg-glow-3 { width: 200px; height: 200px; background: #f97316; opacity: 0.12; top: 55%; left: 55%; animation: glowPulse 12s ease-in-out infinite 1s; }
|
||||
@keyframes glowPulse {
|
||||
0%,100% { transform: scale(1); opacity: 0.2; }
|
||||
50% { transform: scale(1.2); opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* ── Floating score card ── */
|
||||
.lg-score-card {
|
||||
position: absolute;
|
||||
top: 9%; right: 6%;
|
||||
width: 162px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.11), rgba(255,255,255,0.05));
|
||||
border: 1px solid rgba(255,255,255,0.18);
|
||||
border-radius: 20px;
|
||||
padding: 1.1rem 1.2rem 1rem;
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.15);
|
||||
animation: floatA 6s ease-in-out infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
@keyframes floatA {
|
||||
0%,100% { transform: translateY(0) rotate(-1.5deg); }
|
||||
50% { transform: translateY(-14px) rotate(0.5deg); }
|
||||
}
|
||||
.lg-score-tag {
|
||||
font-size: 0.58rem; font-weight: 800; letter-spacing: 0.14em;
|
||||
text-transform: uppercase; color: #7dd3fc; margin-bottom: 0.5rem;
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
}
|
||||
.lg-score-tag::before { content:''; width:6px;height:6px;border-radius:50%;background:#34d399;display:inline-block;box-shadow:0 0 6px #34d399; }
|
||||
.lg-score-num {
|
||||
font-size: 2.2rem; font-weight: 900; color: white; line-height: 1; margin-bottom: 0.2rem;
|
||||
text-shadow: 0 2px 12px rgba(99,179,255,0.4);
|
||||
}
|
||||
.lg-score-delta {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.7rem; font-weight: 700; color: #4ade80;
|
||||
display: flex; align-items: center; gap: 0.2rem; margin-bottom: 0.75rem;
|
||||
}
|
||||
.lg-bars {
|
||||
display: flex; align-items: flex-end; gap: 3px; height: 40px;
|
||||
}
|
||||
.lg-bar {
|
||||
flex: 1; border-radius: 3px 3px 0 0;
|
||||
animation: barGrow 1s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
@keyframes barGrow {
|
||||
from { transform: scaleY(0); }
|
||||
to { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
/* ── Streak pill ── */
|
||||
.lg-streak {
|
||||
position: absolute;
|
||||
bottom: 12%; left: 5%;
|
||||
background: linear-gradient(135deg, rgba(249,115,22,0.18), rgba(239,68,68,0.1));
|
||||
border: 1px solid rgba(249,115,22,0.4);
|
||||
border-radius: 100px;
|
||||
padding: 0.65rem 1.2rem;
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 4px 20px rgba(249,115,22,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
animation: floatB 7s ease-in-out infinite 1s;
|
||||
z-index: 2;
|
||||
}
|
||||
@keyframes floatB {
|
||||
0%,100% { transform: translateY(0) rotate(1deg); }
|
||||
50% { transform: translateY(-10px) rotate(-0.5deg); }
|
||||
}
|
||||
.lg-streak-fire { font-size: 1.5rem; filter: drop-shadow(0 0 8px #f97316); }
|
||||
.lg-streak-text strong { display:block; font-size:0.85rem; font-weight:900; color:white; }
|
||||
.lg-streak-text span { font-family:'Nunito Sans',sans-serif; font-size:0.68rem; font-weight:600; color:#fed7aa; }
|
||||
|
||||
/* ── Questions badge ── */
|
||||
.lg-q-badge {
|
||||
position: absolute;
|
||||
bottom: 28%; right: 5%;
|
||||
background: linear-gradient(135deg, rgba(139,92,246,0.18), rgba(99,102,241,0.1));
|
||||
border: 1px solid rgba(139,92,246,0.4);
|
||||
border-radius: 16px;
|
||||
padding: 0.7rem 1.05rem;
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 4px 20px rgba(139,92,246,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
animation: floatA 9s ease-in-out infinite 0.5s;
|
||||
z-index: 2;
|
||||
}
|
||||
.lg-q-badge p { font-family:'Nunito Sans',sans-serif; font-size:0.68rem; font-weight:700; color:#c4b5fd; margin-bottom:0.15rem; }
|
||||
.lg-q-badge strong { font-size:1.35rem; font-weight:900; color:white; display:block; text-shadow:0 0 20px rgba(167,139,250,0.5); }
|
||||
|
||||
/* ── Accuracy ring ── */
|
||||
.lg-ring-wrap {
|
||||
position: absolute;
|
||||
top: 52%; left: 6%;
|
||||
width: 80px; height: 80px;
|
||||
animation: floatB 10s ease-in-out infinite 0.8s;
|
||||
z-index: 2;
|
||||
}
|
||||
.lg-ring-svg { width: 80px; height: 80px; transform: rotate(-90deg); }
|
||||
.lg-ring-bg { fill: none; stroke: rgba(255,255,255,0.08); stroke-width: 5; }
|
||||
.lg-ring-fill { fill: none; stroke: #34d399; stroke-width: 5; stroke-linecap: round;
|
||||
stroke-dasharray: 188; stroke-dashoffset: 18;
|
||||
animation: ringFill 1.8s ease both 0.3s; }
|
||||
@keyframes ringFill {
|
||||
from { stroke-dashoffset: 188; }
|
||||
to { stroke-dashoffset: 18; }
|
||||
}
|
||||
.lg-ring-label {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
.lg-ring-label strong { font-size: 0.95rem; font-weight: 900; color: white; line-height: 1; }
|
||||
.lg-ring-label span { font-family:'Nunito Sans',sans-serif; font-size: 0.52rem; font-weight: 700; color: #7dd3fc; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
|
||||
/* Scattered glitter dots */
|
||||
.lg-glitter { position: absolute; border-radius: 50%; pointer-events: none; animation: glitterFloat 8s ease-in-out infinite; }
|
||||
@keyframes glitterFloat {
|
||||
0%,100% { transform: translateY(0) scale(1); opacity: 0.5; }
|
||||
50% { transform: translateY(-16px) scale(1.3); opacity: 0.9; }
|
||||
}
|
||||
|
||||
/* Stars */
|
||||
.lg-star { position: absolute; pointer-events: none; animation: starTwinkle 2.5s ease-in-out infinite; color: #fde68a; }
|
||||
@keyframes starTwinkle {
|
||||
0%,100% { opacity: 0.4; transform: scale(0.9) rotate(0deg); }
|
||||
50% { opacity: 1; transform: scale(1.4) rotate(20deg); }
|
||||
}
|
||||
|
||||
/* Thin decorative rings */
|
||||
.lg-deco-ring {
|
||||
position: absolute; border-radius: 50%; pointer-events: none;
|
||||
border: 1.5px solid rgba(255,255,255,0.07);
|
||||
animation: decoSpin 40s linear infinite;
|
||||
}
|
||||
@keyframes decoSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Panel content */
|
||||
.lg-panel-content {
|
||||
position: relative; z-index: 3;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: flex-start; gap: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lg-panel-logo { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.lg-panel-logo-badge {
|
||||
width: 46px; height: 46px; border-radius: 13px;
|
||||
background: linear-gradient(135deg, #f97316, #ef4444);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 2rem 1.25rem;
|
||||
box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 8px 20px rgba(249,115,22,0.45);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.lg-panel-logo-text { font-size: 1.35rem; font-weight: 900; color: white; letter-spacing: -0.02em; }
|
||||
|
||||
.lg-panel-headline { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.lg-panel-headline h2 {
|
||||
font-size: 2.6rem; font-weight: 900; line-height: 1.1;
|
||||
color: white; letter-spacing: -0.035em;
|
||||
text-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.lg-panel-headline h2 span {
|
||||
background: linear-gradient(90deg, #fbbf24 0%, #f97316 60%, #ef4444 100%);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 16px rgba(249,115,22,0.4));
|
||||
}
|
||||
.lg-panel-headline p {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.92rem; font-weight: 600; color: #93c5fd; line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Blobs */
|
||||
.lg-blob { position:fixed;pointer-events:none;z-index:0; }
|
||||
.lg-blob-1 { width:280px;height:280px;background:#fde68a;top:-100px;left:-100px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:lgWobble1 14s ease-in-out infinite; }
|
||||
.lg-blob-2 { width:220px;height:220px;background:#a5f3c0;bottom:-60px;left:4%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 16s ease-in-out infinite; }
|
||||
.lg-blob-3 { width:250px;height:250px;background:#fbcfe8;top:10%;right:-70px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:lgWobble1 18s ease-in-out infinite reverse; }
|
||||
.lg-blob-4 { width:180px;height:180px;background:#bfdbfe;bottom:8%;right:0;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes lgWobble1 {
|
||||
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(14px,18px) rotate(8deg);}
|
||||
/* Stats row */
|
||||
.lg-stats { display: flex; gap: 0.75rem; width: 100%; }
|
||||
.lg-stat {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.09), rgba(255,255,255,0.04));
|
||||
border: 1px solid rgba(255,255,255,0.13);
|
||||
border-radius: 18px; padding: 0.9rem 0.85rem;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.12);
|
||||
display: flex; flex-direction: column; gap: 0.2rem;
|
||||
animation: statSlide 0.5s ease both;
|
||||
}
|
||||
@keyframes lgWobble2 {
|
||||
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(-12px,14px) rotate(-6deg);}
|
||||
.lg-stat:nth-child(1) { animation-delay: 0.1s; }
|
||||
.lg-stat:nth-child(2) { animation-delay: 0.2s; }
|
||||
.lg-stat:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes statSlide {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.lg-stat-icon { width: 28px; height: 28px; border-radius: 8px; display:flex;align-items:center;justify-content:center; margin-bottom:0.3rem; }
|
||||
.lg-stat strong { font-size: 1.3rem; font-weight: 900; color: white; line-height: 1; }
|
||||
.lg-stat span { font-family:'Nunito Sans',sans-serif; font-size:0.64rem; font-weight:600; color:#93c5fd; }
|
||||
|
||||
.lg-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.28;animation:lgFloat 7s ease-in-out infinite; }
|
||||
@keyframes lgFloat {
|
||||
0%,100%{transform:translateY(0) rotate(0deg);}
|
||||
50%{transform:translateY(-14px) rotate(180deg);}
|
||||
/* Social proof */
|
||||
.lg-social { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.lg-avs { display: flex; }
|
||||
.lg-av {
|
||||
width: 30px; height: 30px; border-radius: 50%;
|
||||
border: 2px solid #0f2044; margin-left: -8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.6rem; font-weight: 800; color: white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.lg-av:first-child { margin-left: 0; }
|
||||
.lg-social p { font-family:'Nunito Sans',sans-serif; font-size:0.75rem; font-weight:700; color:#93c5fd; }
|
||||
.lg-social p strong { color:#fbbf24; }
|
||||
|
||||
/* Card */
|
||||
.lg-card {
|
||||
/* ─── RIGHT PANEL ─── */
|
||||
.lg-right {
|
||||
flex: 1;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 3rem 4rem;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.lg-bg-dot { position:absolute;border-radius:50%;pointer-events:none;opacity:0.09;animation:bgFloat 10s ease-in-out infinite; }
|
||||
@keyframes bgFloat { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-14px);} }
|
||||
|
||||
.lg-form-wrap {
|
||||
position: relative; z-index: 1;
|
||||
width: 100%; max-width: 400px;
|
||||
background: white; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04);
|
||||
padding: 2.25rem 2rem 2rem;
|
||||
display: flex; flex-direction: column; gap: 1.75rem;
|
||||
animation: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
display: flex; flex-direction: column; gap: 2rem;
|
||||
animation: formPop 0.55s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
@keyframes lgPopIn {
|
||||
from { opacity:0; transform:scale(0.9) translateY(20px); }
|
||||
to { opacity:1; transform:scale(1) translateY(0); }
|
||||
@keyframes formPop {
|
||||
from { opacity:0; transform:translateY(22px) scale(0.97); }
|
||||
to { opacity:1; transform:translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* Logo area */
|
||||
.lg-logo-wrap {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.85rem;
|
||||
}
|
||||
.lg-logo-badge {
|
||||
width: 64px; height: 64px; border-radius: 20px;
|
||||
background: linear-gradient(135deg, #a855f7, #7c3aed);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 6px 0 #5b21b655, 0 10px 24px rgba(124,58,237,0.25);
|
||||
font-size: 1.75rem;
|
||||
animation: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) 0.1s both;
|
||||
}
|
||||
.lg-title {
|
||||
font-size: 1.5rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.02em; text-align: center;
|
||||
}
|
||||
.lg-sub {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.82rem; font-weight: 600; color: #9ca3af;
|
||||
text-align: center; margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.lg-fields { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.lg-form-header { display:flex;flex-direction:column;gap:0.4rem; }
|
||||
.lg-form-header h1 { font-size:2rem;font-weight:900;color:#1e1b4b;letter-spacing:-0.03em;line-height:1.2; }
|
||||
.lg-form-header p { font-family:'Nunito Sans',sans-serif;font-size:0.88rem;font-weight:600;color:#9ca3af; }
|
||||
|
||||
.lg-fields { display:flex;flex-direction:column;gap:1.1rem; }
|
||||
.lg-field { display:flex;flex-direction:column;gap:0.4rem; }
|
||||
.lg-label {
|
||||
font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em;
|
||||
text-transform: uppercase; color: #6b7280;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
.lg-label { font-size:0.7rem;font-weight:800;letter-spacing:0.1em;text-transform:uppercase;color:#6b7280;padding-left:0.2rem; }
|
||||
.lg-input-wrap { position:relative; }
|
||||
.lg-input-icon {
|
||||
position: absolute; left: 0.85rem; top: 50%;
|
||||
transform: translateY(-50%); pointer-events: none; color: #9ca3af;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.lg-input-icon { position:absolute;left:0.9rem;top:50%;transform:translateY(-50%);pointer-events:none;color:#9ca3af;transition:color 0.2s; }
|
||||
.lg-input {
|
||||
width: 100%; padding: 0.8rem 1rem 0.8rem 2.6rem;
|
||||
background: #f9fafb; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 14px;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.88rem; font-weight: 600; color: #1e1b4b;
|
||||
outline: none; transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
width:100%;padding:0.9rem 1rem 0.9rem 2.65rem;
|
||||
background:#f9fafb;border:2.5px solid #f3f4f6;border-radius:14px;
|
||||
font-family:'Nunito Sans',sans-serif;font-size:0.9rem;font-weight:600;color:#1e1b4b;
|
||||
outline:none;transition:all 0.2s;box-sizing:border-box;
|
||||
}
|
||||
.lg-input:focus {
|
||||
background: white; border-color: #c4b5fd;
|
||||
box-shadow: 0 0 0 3px rgba(168,85,247,0.1);
|
||||
}
|
||||
.lg-input:focus ~ .lg-input-icon { color: #a855f7; }
|
||||
.lg-input:focus { background:white;border-color:#93c5fd;box-shadow:0 0 0 3.5px rgba(59,130,246,0.1); }
|
||||
.lg-input:disabled { opacity:0.5;cursor:not-allowed; }
|
||||
.lg-input::placeholder { color:#d1d5db; }
|
||||
|
||||
/* Remember me */
|
||||
.lg-remember {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0 0.1rem;
|
||||
}
|
||||
.lg-checkbox {
|
||||
width: 18px; height: 18px; border-radius: 6px;
|
||||
accent-color: #a855f7; cursor: pointer; flex-shrink: 0;
|
||||
}
|
||||
.lg-remember-label {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.8rem; font-weight: 600; color: #6b7280;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lg-remember { display:flex;align-items:center;gap:0.5rem;padding:0 0.1rem; }
|
||||
.lg-checkbox { width:17px;height:17px;border-radius:5px;accent-color:#3b82f6;cursor:pointer;flex-shrink:0; }
|
||||
.lg-remember-label { font-family:'Nunito Sans',sans-serif;font-size:0.8rem;font-weight:600;color:#6b7280;cursor:pointer; }
|
||||
|
||||
/* Error */
|
||||
.lg-error {
|
||||
background: #fff1f2; border: 2px solid #fecdd3;
|
||||
border-radius: 14px; padding: 0.75rem 1rem;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.82rem; font-weight: 700; color: #e11d48;
|
||||
background:#fff1f2;border:2px solid #fecdd3;border-radius:14px;padding:0.8rem 1rem;
|
||||
font-family:'Nunito Sans',sans-serif;font-size:0.82rem;font-weight:700;color:#e11d48;
|
||||
display:flex;align-items:center;gap:0.5rem;
|
||||
}
|
||||
|
||||
/* Submit button */
|
||||
.lg-btn {
|
||||
width: 100%; padding: 0.95rem;
|
||||
background: #f97316; color: white; border: none;
|
||||
border-radius: 100px; cursor: pointer;
|
||||
font-family: 'Nunito', sans-serif; font-size: 0.95rem; font-weight: 900;
|
||||
width:100%;padding:1rem;background:#f97316;color:white;border:none;border-radius:100px;cursor:pointer;
|
||||
font-family:'Nunito',sans-serif;font-size:1rem;font-weight:900;
|
||||
display:flex;align-items:center;justify-content:center;gap:0.5rem;
|
||||
box-shadow: 0 6px 0 #c2560e, 0 8px 20px rgba(249,115,22,0.25);
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
box-shadow:0 6px 0 #c2560e,0 10px 24px rgba(249,115,22,0.28);
|
||||
transition:transform 0.1s,box-shadow 0.1s;
|
||||
}
|
||||
.lg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #c2560e,0 12px 24px rgba(249,115,22,0.3); }
|
||||
.lg-btn:hover { transform:translateY(-2px);box-shadow:0 8px 0 #c2560e,0 14px 28px rgba(249,115,22,0.32); }
|
||||
.lg-btn:active { transform:translateY(3px);box-shadow:0 3px 0 #c2560e; }
|
||||
.lg-btn:disabled {
|
||||
background: #e5e7eb; color: #9ca3af;
|
||||
cursor: not-allowed; box-shadow: 0 4px 0 #d1d5db;
|
||||
}
|
||||
.lg-btn:disabled { background:#e5e7eb;color:#9ca3af;cursor:not-allowed;box-shadow:0 4px 0 #d1d5db; }
|
||||
.lg-btn:disabled:hover { transform:none;box-shadow:0 4px 0 #d1d5db; }
|
||||
|
||||
.lg-spinner { animation: lgSpin 0.8s linear infinite; }
|
||||
@keyframes lgSpin { to { transform: rotate(360deg); } }
|
||||
.lg-spinner { animation:spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform:rotate(360deg); } }
|
||||
|
||||
/* Footer hint */
|
||||
.lg-footer {
|
||||
text-align: center;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.75rem; font-weight: 600; color: #9ca3af;
|
||||
.lg-footer { text-align:center;font-family:'Nunito Sans',sans-serif;font-size:0.75rem;font-weight:600;color:#9ca3af; }
|
||||
.lg-signup-footer { text-align:center;font-family:'Nunito Sans',sans-serif;font-size:0.8rem;font-weight:600;color:#9ca3af; }
|
||||
.lg-link { color:#f97316;font-weight:800;text-decoration:none;cursor:pointer; }
|
||||
.lg-link:hover { color:#ea6c00; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.lg-left { display:none; }
|
||||
.lg-right { padding:2rem 1.5rem; }
|
||||
}
|
||||
.rg-footer {
|
||||
text-align: center;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.78rem; font-weight: 600; color: #9ca3af;
|
||||
}
|
||||
.rg-link {
|
||||
color: #a855f7; font-weight: 800; text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.rg-link:hover { color: #7c3aed; }
|
||||
`;
|
||||
|
||||
const BAR_HEIGHTS = [30, 50, 42, 75, 55, 88, 65];
|
||||
const BAR_COLORS = [
|
||||
"#60a5fa",
|
||||
"#60a5fa",
|
||||
"#7dd3fc",
|
||||
"#38bdf8",
|
||||
"#60a5fa",
|
||||
"#7dd3fc",
|
||||
"#38bdf8",
|
||||
];
|
||||
|
||||
const AV_COLORS = [
|
||||
"linear-gradient(135deg,#3b82f6,#1d4ed8)",
|
||||
"linear-gradient(135deg,#f97316,#ef4444)",
|
||||
"linear-gradient(135deg,#22c55e,#15803d)",
|
||||
"linear-gradient(135deg,#a855f7,#7c3aed)",
|
||||
"linear-gradient(135deg,#eab308,#ca8a04)",
|
||||
];
|
||||
const AV_INITIALS = ["SK", "NR", "TM", "AB", "PL"];
|
||||
|
||||
export const Login = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@ -195,13 +367,12 @@ export const Login = () => {
|
||||
|
||||
const { login, isAuthenticated, isLoading, error, clearError } =
|
||||
useAuthStore();
|
||||
|
||||
const from = (location.state as LocationState)?.from?.pathname || "/student";
|
||||
const from =
|
||||
(location.state as LocationState)?.from?.pathname || "/student/home";
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) navigate("/student/home", { replace: true });
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearError();
|
||||
}, [clearError]);
|
||||
@ -216,61 +387,249 @@ export const Login = () => {
|
||||
if (isAuthenticated) return null;
|
||||
|
||||
return (
|
||||
<div className="lg-screen">
|
||||
<div className="lg-root">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
{/* Blobs */}
|
||||
<div className="lg-blob lg-blob-1" />
|
||||
<div className="lg-blob lg-blob-2" />
|
||||
<div className="lg-blob lg-blob-3" />
|
||||
<div className="lg-blob lg-blob-4" />
|
||||
{/* ── LEFT PANEL ── */}
|
||||
<div className="lg-left">
|
||||
{/* Background layers */}
|
||||
<div className="lg-grid" />
|
||||
<div className="lg-glow lg-glow-1" />
|
||||
<div className="lg-glow lg-glow-2" />
|
||||
<div className="lg-glow lg-glow-3" />
|
||||
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
{/* Decorative spinning rings */}
|
||||
{[
|
||||
{ s: 260, t: "38%", l: "58%", mt: -130, ml: -130 },
|
||||
{
|
||||
s: 380,
|
||||
t: "42%",
|
||||
l: "54%",
|
||||
mt: -190,
|
||||
ml: -190,
|
||||
dir: "reverse" as const,
|
||||
},
|
||||
].map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="lg-dot"
|
||||
className="lg-deco-ring"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
top: d.top,
|
||||
left: d.left,
|
||||
right: d.right,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: `${5.5 + i * 0.4}s`,
|
||||
width: r.s,
|
||||
height: r.s,
|
||||
top: r.t,
|
||||
left: r.l,
|
||||
marginTop: r.mt,
|
||||
marginLeft: r.ml,
|
||||
animationDirection: r.dir ?? "normal",
|
||||
animationDuration: `${35 + i * 10}s`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="lg-card">
|
||||
{/* Logo + heading */}
|
||||
<div className="lg-logo-wrap space-y-5">
|
||||
<img
|
||||
src="src/assets/ed_logo.png"
|
||||
alt="EdBridge"
|
||||
{/* Floating score card */}
|
||||
<div className="lg-score-card">
|
||||
<p className="lg-score-tag">SAT Score</p>
|
||||
<p className="lg-score-num">1480</p>
|
||||
<p className="lg-score-delta">▲ +120 pts this month</p>
|
||||
<div className="lg-bars">
|
||||
{BAR_HEIGHTS.map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="lg-bar"
|
||||
style={{
|
||||
width: 600,
|
||||
height: 70,
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
height: `${h}%`,
|
||||
background: BAR_COLORS[i],
|
||||
animationDelay: `${i * 0.08}s`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h1 className="lg-title">Welcome back 👋</h1>
|
||||
<p className="lg-sub">Sign in to continue your SAT prep</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fields */}
|
||||
{/* Accuracy ring */}
|
||||
<div className="lg-ring-wrap">
|
||||
<svg className="lg-ring-svg" viewBox="0 0 80 80">
|
||||
<circle className="lg-ring-bg" cx="40" cy="40" r="30" />
|
||||
<circle className="lg-ring-fill" cx="40" cy="40" r="30" />
|
||||
</svg>
|
||||
<div className="lg-ring-label">
|
||||
<strong>94%</strong>
|
||||
<span>Accuracy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Streak pill */}
|
||||
<div className="lg-streak">
|
||||
<span className="lg-streak-fire">🔥</span>
|
||||
<div className="lg-streak-text">
|
||||
<strong>14-day streak!</strong>
|
||||
<span>Keep it going</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Questions badge */}
|
||||
<div className="lg-q-badge">
|
||||
<p>Questions solved</p>
|
||||
<strong>2,847</strong>
|
||||
</div>
|
||||
|
||||
{/* Glitter dots */}
|
||||
{[
|
||||
{ s: 10, c: "#60a5fa", t: "13%", l: "58%", d: "0s", dur: "9s" },
|
||||
{ s: 7, c: "#fbbf24", t: "70%", l: "70%", d: "1s", dur: "11s" },
|
||||
{ s: 12, c: "#34d399", t: "38%", l: "7%", d: "0.4s", dur: "8s" },
|
||||
{ s: 5, c: "#f472b6", t: "82%", l: "35%", d: "1.8s", dur: "13s" },
|
||||
{ s: 8, c: "#a78bfa", t: "20%", l: "36%", d: "0.9s", dur: "10s" },
|
||||
{ s: 6, c: "#fb923c", t: "62%", l: "80%", d: "1.3s", dur: "7s" },
|
||||
].map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="lg-glitter"
|
||||
style={
|
||||
{
|
||||
width: d.s,
|
||||
height: d.s,
|
||||
background: d.c,
|
||||
top: d.t,
|
||||
left: d.l,
|
||||
animationDelay: d.d,
|
||||
animationDuration: d.dur,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Stars */}
|
||||
{[
|
||||
{ t: "8%", l: "16%", s: "1.1rem", d: "0s" },
|
||||
{ t: "22%", l: "74%", s: "0.85rem", d: "0.7s" },
|
||||
{ t: "55%", l: "20%", s: "0.95rem", d: "1.4s" },
|
||||
{ t: "86%", l: "58%", s: "0.75rem", d: "0.3s" },
|
||||
{ t: "44%", l: "88%", s: "1rem", d: "1.0s" },
|
||||
].map((s, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="lg-star"
|
||||
style={
|
||||
{
|
||||
top: s.t,
|
||||
left: s.l,
|
||||
fontSize: s.s,
|
||||
animationDelay: s.d,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
|
||||
{/* Panel content */}
|
||||
<div className="lg-panel-content">
|
||||
<div className="lg-panel-logo">
|
||||
<div className="lg-panel-logo-badge">📚</div>
|
||||
<span className="lg-panel-logo-text">EdBridge</span>
|
||||
</div>
|
||||
|
||||
<div className="lg-panel-headline">
|
||||
<h2>
|
||||
Welcome
|
||||
<br />
|
||||
back,
|
||||
<br />
|
||||
<span>champion.</span>
|
||||
</h2>
|
||||
<p>
|
||||
Your SAT goals are waiting.
|
||||
<br />
|
||||
Pick up right where you left off.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg-stats">
|
||||
{[
|
||||
{
|
||||
icon: <Target size={14} color="#fff" />,
|
||||
bg: "linear-gradient(135deg,#3b82f6,#1d4ed8)",
|
||||
val: "94%",
|
||||
label: "Accuracy",
|
||||
},
|
||||
{
|
||||
icon: <Clock size={14} color="#fff" />,
|
||||
bg: "linear-gradient(135deg,#f97316,#ef4444)",
|
||||
val: "47m",
|
||||
label: "Daily study",
|
||||
},
|
||||
{
|
||||
icon: <BarChart2 size={14} color="#fff" />,
|
||||
bg: "linear-gradient(135deg,#22c55e,#15803d)",
|
||||
val: "+180",
|
||||
label: "Score gain",
|
||||
},
|
||||
].map((s, i) => (
|
||||
<div className="lg-stat" key={i}>
|
||||
<div className="lg-stat-icon" style={{ background: s.bg }}>
|
||||
{s.icon}
|
||||
</div>
|
||||
<strong>{s.val}</strong>
|
||||
<span>{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="lg-social">
|
||||
<div className="lg-avs">
|
||||
{AV_INITIALS.map((init, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="lg-av"
|
||||
style={{ background: AV_COLORS[i] }}
|
||||
>
|
||||
{init}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>
|
||||
<strong>2,400+</strong> students improved their scores
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── RIGHT PANEL ── */}
|
||||
<div className="lg-right">
|
||||
{[
|
||||
{ s: 200, c: "#f97316", t: "4%", r: "4%", d: "0s", dur: "12s" },
|
||||
{ s: 120, c: "#3b82f6", b: "8%", l: "2%", d: "1.5s", dur: "10s" },
|
||||
{ s: 70, c: "#22c55e", t: "52%", r: "2%", d: "0.8s", dur: "8s" },
|
||||
].map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="lg-bg-dot"
|
||||
style={
|
||||
{
|
||||
width: d.s,
|
||||
height: d.s,
|
||||
background: d.c,
|
||||
top: (d as any).t,
|
||||
right: (d as any).r,
|
||||
bottom: (d as any).b,
|
||||
left: (d as any).l,
|
||||
animationDelay: d.d,
|
||||
animationDuration: d.dur,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="lg-form-wrap">
|
||||
<div className="lg-form-header">
|
||||
<h1>Welcome back 👋</h1>
|
||||
<p>Sign in to continue your SAT prep</p>
|
||||
</div>
|
||||
|
||||
<div className="lg-fields">
|
||||
{/* Email */}
|
||||
<div className="lg-field">
|
||||
<label className="lg-label" htmlFor="email">
|
||||
Email
|
||||
@ -289,7 +648,6 @@ export const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="lg-field">
|
||||
<label className="lg-label" htmlFor="password">
|
||||
Password
|
||||
@ -308,7 +666,6 @@ export const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remember me */}
|
||||
<div className="lg-remember">
|
||||
<input id="rememberMe" type="checkbox" className="lg-checkbox" />
|
||||
<label htmlFor="rememberMe" className="lg-remember-label">
|
||||
@ -316,14 +673,12 @@ export const Login = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="lg-error">
|
||||
<span>⚠️</span> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
className="lg-btn"
|
||||
onClick={handleSubmit}
|
||||
@ -340,15 +695,16 @@ export const Login = () => {
|
||||
</div>
|
||||
|
||||
<p className="lg-footer">
|
||||
By signing in you agree to Edbridge's Terms & Privacy Policy.
|
||||
By signing in you agree to EdBridge's Terms & Privacy Policy.
|
||||
</p>
|
||||
<p className="rg-footer">
|
||||
<p className="lg-signup-footer">
|
||||
Don't have an account?{" "}
|
||||
<a href="/register" className="rg-link">
|
||||
<span className="lg-link" onClick={() => navigate("/register")}>
|
||||
Sign up
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,173 +2,333 @@ import { useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import { Loader2, Mail, Lock, User, ImageIcon } from "lucide-react";
|
||||
|
||||
const DOTS = [
|
||||
{ size: 12, color: "#f97316", top: "8%", left: "6%", delay: "0s" },
|
||||
{ size: 7, color: "#a855f7", top: "22%", left: "3%", delay: "1.2s" },
|
||||
{ size: 9, color: "#22c55e", top: "65%", left: "5%", delay: "0.6s" },
|
||||
{ size: 8, color: "#f43f5e", top: "80%", left: "8%", delay: "2.1s" },
|
||||
{ size: 12, color: "#3b82f6", top: "10%", right: "6%", delay: "1.8s" },
|
||||
{ size: 7, color: "#eab308", top: "40%", right: "3%", delay: "0.9s" },
|
||||
{ size: 10, color: "#a855f7", top: "72%", right: "5%", delay: "0.4s" },
|
||||
{ size: 8, color: "#f97316", top: "55%", right: "8%", delay: "1.5s" },
|
||||
];
|
||||
import {
|
||||
Loader2,
|
||||
Mail,
|
||||
Lock,
|
||||
User,
|
||||
ImageIcon,
|
||||
BookOpen,
|
||||
Star,
|
||||
Zap,
|
||||
Trophy,
|
||||
} from "lucide-react";
|
||||
|
||||
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');
|
||||
|
||||
.rg-screen {
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
.rg-root {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
display: flex;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
background: #fffbf4;
|
||||
}
|
||||
|
||||
/* ─── LEFT PANEL ─── */
|
||||
.rg-left {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(150deg, #1e1b4b 0%, #3b1d8a 50%, #6d28d9 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 3rem 3.5rem;
|
||||
overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 2rem 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Blobs */
|
||||
.rg-blob { position:fixed;pointer-events:none;z-index:0; }
|
||||
.rg-blob-1 { width:280px;height:280px;background:#fde68a;top:-100px;left:-100px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:rgWobble1 14s ease-in-out infinite; }
|
||||
.rg-blob-2 { width:220px;height:220px;background:#a5f3c0;bottom:-60px;left:4%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:rgWobble2 16s ease-in-out infinite; }
|
||||
.rg-blob-3 { width:250px;height:250px;background:#fbcfe8;top:10%;right:-70px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:rgWobble1 18s ease-in-out infinite reverse; }
|
||||
.rg-blob-4 { width:180px;height:180px;background:#bfdbfe;bottom:8%;right:0;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:rgWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes rgWobble1 {
|
||||
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(14px,18px) rotate(8deg);}
|
||||
/* Blobs inside left panel */
|
||||
.rg-panel-blob {
|
||||
position: absolute; pointer-events: none; border-radius: 50%;
|
||||
opacity: 0.18;
|
||||
}
|
||||
@keyframes rgWobble2 {
|
||||
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(-12px,14px) rotate(-6deg);}
|
||||
.rg-panel-blob-1 {
|
||||
width: 420px; height: 420px;
|
||||
background: #a855f7;
|
||||
top: -140px; right: -100px;
|
||||
animation: blobDrift1 16s ease-in-out infinite;
|
||||
}
|
||||
.rg-panel-blob-2 {
|
||||
width: 300px; height: 300px;
|
||||
background: #f97316;
|
||||
bottom: -100px; left: -80px;
|
||||
animation: blobDrift2 18s ease-in-out infinite;
|
||||
}
|
||||
.rg-panel-blob-3 {
|
||||
width: 200px; height: 200px;
|
||||
background: #22c55e;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: blobDrift1 14s ease-in-out infinite reverse;
|
||||
}
|
||||
@keyframes blobDrift1 {
|
||||
0%,100% { transform: translate(0,0) scale(1); }
|
||||
50% { transform: translate(24px, 32px) scale(1.08); }
|
||||
}
|
||||
@keyframes blobDrift2 {
|
||||
0%,100% { transform: translate(0,0) scale(1); }
|
||||
50% { transform: translate(-20px, -28px) scale(1.06); }
|
||||
}
|
||||
|
||||
.rg-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.28;animation:rgFloat 7s ease-in-out infinite; }
|
||||
@keyframes rgFloat {
|
||||
/* Floating decorative shapes */
|
||||
.rg-shape {
|
||||
position: absolute; pointer-events: none;
|
||||
animation: floatShape 8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes floatShape {
|
||||
0%,100% { transform: translateY(0) rotate(0deg); }
|
||||
50%{transform:translateY(-14px) rotate(180deg);}
|
||||
50% { transform: translateY(-16px) rotate(12deg); }
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.rg-card {
|
||||
/* Stars scattered */
|
||||
.rg-star {
|
||||
position: absolute; pointer-events: none;
|
||||
color: #fde68a; opacity: 0.55;
|
||||
animation: twinkle 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes twinkle {
|
||||
0%,100% { opacity: 0.55; transform: scale(1); }
|
||||
50% { opacity: 0.9; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
/* Left panel content */
|
||||
.rg-panel-content {
|
||||
position: relative; z-index: 1;
|
||||
width: 100%; max-width: 400px;
|
||||
background: white; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04);
|
||||
padding: 2.25rem 2rem 2rem;
|
||||
display: flex; flex-direction: column; gap: 1.75rem;
|
||||
animation: rgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
@keyframes rgPopIn {
|
||||
from { opacity:0; transform:scale(0.9) translateY(20px); }
|
||||
to { opacity:1; transform:scale(1) translateY(0); }
|
||||
display: flex; flex-direction: column;
|
||||
align-items: flex-start; gap: 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Logo area */
|
||||
.rg-logo-wrap {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.85rem;
|
||||
.rg-panel-logo {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
}
|
||||
.rg-title {
|
||||
font-size: 1.5rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.02em; text-align: center;
|
||||
.rg-panel-logo-badge {
|
||||
width: 48px; height: 48px; border-radius: 14px;
|
||||
background: linear-gradient(135deg, #f97316, #ef4444);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 6px 0 rgba(0,0,0,0.25), 0 8px 20px rgba(249,115,22,0.4);
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.rg-sub {
|
||||
.rg-panel-logo-text {
|
||||
font-size: 1.4rem; font-weight: 900;
|
||||
color: white; letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.rg-panel-headline {
|
||||
display: flex; flex-direction: column; gap: 0.75rem;
|
||||
}
|
||||
.rg-panel-headline h2 {
|
||||
font-size: 2.4rem; font-weight: 900; line-height: 1.15;
|
||||
color: white; letter-spacing: -0.03em;
|
||||
}
|
||||
.rg-panel-headline h2 span {
|
||||
background: linear-gradient(90deg, #fde68a, #f97316);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
}
|
||||
.rg-panel-headline p {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.82rem; font-weight: 600; color: #9ca3af;
|
||||
text-align: center; margin-top: -0.25rem;
|
||||
font-size: 1rem; font-weight: 600; color: #c4b5fd; line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Avatar preview */
|
||||
.rg-avatar-preview {
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
/* Feature pills */
|
||||
.rg-features { display: flex; flex-direction: column; gap: 0.85rem; }
|
||||
.rg-feature {
|
||||
display: flex; align-items: center; gap: 0.85rem;
|
||||
background: rgba(255,255,255,0.07);
|
||||
border: 1.5px solid rgba(255,255,255,0.12);
|
||||
border-radius: 14px; padding: 0.85rem 1.1rem;
|
||||
backdrop-filter: blur(8px);
|
||||
animation: fadeSlideIn 0.5s ease both;
|
||||
}
|
||||
.rg-feature:nth-child(1) { animation-delay: 0.1s; }
|
||||
.rg-feature:nth-child(2) { animation-delay: 0.2s; }
|
||||
.rg-feature:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes fadeSlideIn {
|
||||
from { opacity: 0; transform: translateX(-16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.rg-feature-icon {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rg-feature-text strong {
|
||||
display: block; font-size: 0.85rem; font-weight: 800; color: white;
|
||||
}
|
||||
.rg-feature-text span {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.75rem; font-weight: 600; color: #a5b4fc;
|
||||
}
|
||||
|
||||
/* Social proof */
|
||||
.rg-social-proof {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
.rg-avatars { display: flex; }
|
||||
.rg-av {
|
||||
width: 30px; height: 30px; border-radius: 50%;
|
||||
border: 2px solid #3b1d8a;
|
||||
background: linear-gradient(135deg, #a855f7, #6d28d9);
|
||||
margin-left: -8px; display: flex; align-items: center;
|
||||
justify-content: center; font-size: 0.65rem; font-weight: 800; color: white;
|
||||
}
|
||||
.rg-av:first-child { margin-left: 0; }
|
||||
.rg-social-proof p {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.78rem; font-weight: 700; color: #c4b5fd;
|
||||
}
|
||||
.rg-social-proof p strong { color: #fde68a; }
|
||||
|
||||
/* ─── RIGHT PANEL (form) ─── */
|
||||
.rg-right {
|
||||
flex: 1;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 3rem 4rem;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
|
||||
/* Subtle bg dots on right */
|
||||
.rg-bg-dot {
|
||||
position: absolute; border-radius: 50%; pointer-events: none; opacity: 0.10;
|
||||
animation: bgDotFloat 9s ease-in-out infinite;
|
||||
}
|
||||
@keyframes bgDotFloat {
|
||||
0%,100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
.rg-form-wrap {
|
||||
position: relative; z-index: 1;
|
||||
width: 100%; max-width: 420px;
|
||||
display: flex; flex-direction: column; gap: 2rem;
|
||||
animation: formPopIn 0.55s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
@keyframes formPopIn {
|
||||
from { opacity: 0; transform: translateY(24px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* Form header */
|
||||
.rg-form-header { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.rg-form-header h1 {
|
||||
font-size: 2rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.03em; line-height: 1.2;
|
||||
}
|
||||
.rg-form-header p {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.88rem; font-weight: 600; color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Avatar row */
|
||||
.rg-avatar-row {
|
||||
display: flex; align-items: center; gap: 1.1rem;
|
||||
background: #f9fafb; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 18px; padding: 0.9rem 1.1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.rg-avatar-row:focus-within { border-color: #c4b5fd; background: white; }
|
||||
.rg-avatar-ring {
|
||||
width: 52px; height: 52px; border-radius: 50%;
|
||||
border: 2.5px dashed #e5e7eb;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden; background: #f9fafb;
|
||||
transition: border-color 0.2s ease;
|
||||
margin-top: 0.25rem;
|
||||
overflow: hidden; background: white; flex-shrink: 0;
|
||||
transition: border-color 0.25s, border-style 0.25s;
|
||||
}
|
||||
.rg-avatar-preview.has-image {
|
||||
border-style: solid; border-color: #c4b5fd;
|
||||
.rg-avatar-ring.filled { border-style: solid; border-color: #a855f7; }
|
||||
.rg-avatar-ring img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.rg-avatar-input-col { flex: 1; display: flex; flex-direction: column; gap: 0.2rem; }
|
||||
.rg-avatar-label {
|
||||
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.1em;
|
||||
text-transform: uppercase; color: #6b7280;
|
||||
}
|
||||
.rg-avatar-preview img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
.rg-avatar-input {
|
||||
background: transparent; border: none; outline: none;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.85rem; font-weight: 600; color: #1e1b4b;
|
||||
width: 100%;
|
||||
}
|
||||
.rg-avatar-input::placeholder { color: #d1d5db; }
|
||||
.rg-avatar-hint {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.7rem; font-weight: 600; color: #c4b5fd;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
/* Fields grid */
|
||||
.rg-fields { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.rg-row { display: flex; gap: 1rem; }
|
||||
.rg-row .rg-field { flex: 1; }
|
||||
|
||||
.rg-field { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.rg-label {
|
||||
font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em;
|
||||
text-transform: uppercase; color: #6b7280;
|
||||
padding-left: 0.25rem;
|
||||
font-size: 0.7rem; font-weight: 800; letter-spacing: 0.1em;
|
||||
text-transform: uppercase; color: #6b7280; padding-left: 0.2rem;
|
||||
}
|
||||
.rg-input-wrap { position: relative; }
|
||||
.rg-input-icon {
|
||||
position: absolute; left: 0.85rem; top: 50%;
|
||||
position: absolute; left: 0.9rem; top: 50%;
|
||||
transform: translateY(-50%); pointer-events: none; color: #9ca3af;
|
||||
transition: color 0.2s ease;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.rg-input {
|
||||
width: 100%; padding: 0.8rem 1rem 0.8rem 2.6rem;
|
||||
width: 100%; padding: 0.85rem 1rem 0.85rem 2.6rem;
|
||||
background: #f9fafb; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 14px;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.88rem; font-weight: 600; color: #1e1b4b;
|
||||
outline: none; transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
outline: none; transition: all 0.2s;
|
||||
}
|
||||
.rg-input:focus {
|
||||
background: white; border-color: #c4b5fd;
|
||||
box-shadow: 0 0 0 3px rgba(168,85,247,0.1);
|
||||
box-shadow: 0 0 0 3.5px rgba(168,85,247,0.1);
|
||||
}
|
||||
.rg-input:focus ~ .rg-input-icon { color: #a855f7; }
|
||||
.rg-input:focus + .rg-input-icon { color: #a855f7; }
|
||||
.rg-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.rg-input::placeholder { color: #d1d5db; }
|
||||
|
||||
/* Password strength */
|
||||
.rg-strength-bar {
|
||||
display: flex; gap: 4px; margin-top: 0.35rem; padding: 0 0.1rem;
|
||||
}
|
||||
/* Strength */
|
||||
.rg-strength-bar { display: flex; gap: 5px; margin-top: 0.4rem; }
|
||||
.rg-strength-seg {
|
||||
flex: 1; height: 4px; border-radius: 999px;
|
||||
background: #f3f4f6; transition: background 0.3s ease;
|
||||
background: #f3f4f6; transition: background 0.3s;
|
||||
}
|
||||
.rg-strength-seg.active-weak { background: #f43f5e; }
|
||||
.rg-strength-seg.active-medium { background: #eab308; }
|
||||
.rg-strength-seg.active-strong { background: #22c55e; }
|
||||
.rg-strength-label {
|
||||
.rg-strength-seg.weak { background: #f43f5e; }
|
||||
.rg-strength-seg.medium { background: #eab308; }
|
||||
.rg-strength-seg.strong { background: #22c55e; }
|
||||
.rg-strength-hint {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.72rem; font-weight: 700;
|
||||
padding: 0 0.1rem; margin-top: 0.15rem;
|
||||
font-size: 0.7rem; font-weight: 700; margin-top: 0.2rem; padding-left: 0.1rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.rg-strength-label.weak { color: #f43f5e; }
|
||||
.rg-strength-label.medium { color: #eab308; }
|
||||
.rg-strength-label.strong { color: #22c55e; }
|
||||
.rg-strength-hint.weak { color: #f43f5e; }
|
||||
.rg-strength-hint.medium { color: #eab308; }
|
||||
.rg-strength-hint.strong { color: #22c55e; }
|
||||
|
||||
/* Error */
|
||||
.rg-error {
|
||||
background: #fff1f2; border: 2px solid #fecdd3;
|
||||
border-radius: 14px; padding: 0.75rem 1rem;
|
||||
border-radius: 14px; padding: 0.8rem 1rem;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.82rem; font-weight: 700; color: #e11d48;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Submit button */
|
||||
/* Submit */
|
||||
.rg-btn {
|
||||
width: 100%; padding: 0.95rem;
|
||||
width: 100%; padding: 1rem;
|
||||
background: #a855f7; color: white; border: none;
|
||||
border-radius: 100px; cursor: pointer;
|
||||
font-family: 'Nunito', sans-serif; font-size: 0.95rem; font-weight: 900;
|
||||
font-family: 'Nunito', sans-serif; font-size: 1rem; font-weight: 900;
|
||||
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
|
||||
box-shadow: 0 6px 0 #7c3aed, 0 8px 20px rgba(168,85,247,0.25);
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
box-shadow: 0 6px 0 #7c3aed, 0 10px 24px rgba(168,85,247,0.3);
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.rg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #7c3aed,0 12px 24px rgba(168,85,247,0.3); }
|
||||
.rg-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 0 #7c3aed, 0 14px 28px rgba(168,85,247,0.35); }
|
||||
.rg-btn:active { transform: translateY(3px); box-shadow: 0 3px 0 #7c3aed; }
|
||||
.rg-btn:disabled {
|
||||
background: #e5e7eb; color: #9ca3af;
|
||||
@ -176,33 +336,121 @@ const STYLES = `
|
||||
}
|
||||
.rg-btn:disabled:hover { transform: none; box-shadow: 0 4px 0 #d1d5db; }
|
||||
|
||||
.rg-spinner { animation: rgSpin 0.8s linear infinite; }
|
||||
@keyframes rgSpin { to { transform: rotate(360deg); } }
|
||||
.rg-spinner { animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Sign-in link */
|
||||
.rg-footer {
|
||||
.rg-form-footer {
|
||||
text-align: center;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.78rem; font-weight: 600; color: #9ca3af;
|
||||
font-size: 0.8rem; font-weight: 600; color: #9ca3af;
|
||||
}
|
||||
.rg-link {
|
||||
color: #a855f7; font-weight: 800; text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.rg-link:hover { color: #7c3aed; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 860px) {
|
||||
.rg-left { display: none; }
|
||||
.rg-right { padding: 2rem 1.5rem; }
|
||||
}
|
||||
`;
|
||||
|
||||
function getPasswordStrength(password: string): 0 | 1 | 2 | 3 {
|
||||
if (!password) return 0;
|
||||
let score = 0;
|
||||
if (password.length >= 8) score++;
|
||||
if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++;
|
||||
if (/[0-9]/.test(password) || /[^A-Za-z0-9]/.test(password)) score++;
|
||||
return score as 0 | 1 | 2 | 3;
|
||||
function getStrength(p: string): 0 | 1 | 2 | 3 {
|
||||
if (!p) return 0;
|
||||
let s = 0;
|
||||
if (p.length >= 8) s++;
|
||||
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++;
|
||||
if (/[0-9]/.test(p) || /[^A-Za-z0-9]/.test(p)) s++;
|
||||
return s as 0 | 1 | 2 | 3;
|
||||
}
|
||||
const S_LABEL = ["", "Weak", "Medium", "Strong"];
|
||||
const S_CLASS = ["", "weak", "medium", "strong"];
|
||||
|
||||
const STRENGTH_LABELS = ["", "Weak", "Medium", "Strong"];
|
||||
const STRENGTH_CLASSES = ["", "weak", "medium", "strong"];
|
||||
const FEATURES = [
|
||||
{
|
||||
icon: <BookOpen size={18} color="#fff" />,
|
||||
bg: "#a855f7",
|
||||
title: "Adaptive Practice",
|
||||
sub: "Questions tailored to your skill level",
|
||||
},
|
||||
{
|
||||
icon: <Zap size={18} color="#fff" />,
|
||||
bg: "#f97316",
|
||||
title: "Instant Feedback",
|
||||
sub: "Know exactly where you went wrong",
|
||||
},
|
||||
{
|
||||
icon: <Trophy size={18} color="#fff" />,
|
||||
bg: "#22c55e",
|
||||
title: "Score Tracking",
|
||||
sub: "Watch your SAT score climb over time",
|
||||
},
|
||||
];
|
||||
|
||||
const PANEL_DOTS = [
|
||||
{
|
||||
size: 70,
|
||||
color: "#f97316",
|
||||
top: "15%",
|
||||
left: "65%",
|
||||
delay: "0s",
|
||||
dur: "9s",
|
||||
},
|
||||
{
|
||||
size: 45,
|
||||
color: "#22c55e",
|
||||
top: "62%",
|
||||
left: "10%",
|
||||
delay: "1s",
|
||||
dur: "11s",
|
||||
},
|
||||
{
|
||||
size: 30,
|
||||
color: "#fde68a",
|
||||
top: "35%",
|
||||
left: "75%",
|
||||
delay: "0.5s",
|
||||
dur: "7s",
|
||||
},
|
||||
{
|
||||
size: 20,
|
||||
color: "#a855f7",
|
||||
top: "78%",
|
||||
left: "50%",
|
||||
delay: "2s",
|
||||
dur: "13s",
|
||||
},
|
||||
];
|
||||
|
||||
const BG_DOTS = [
|
||||
{
|
||||
size: 180,
|
||||
color: "#a855f7",
|
||||
top: "5%",
|
||||
right: "5%",
|
||||
delay: "0s",
|
||||
dur: "12s",
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
color: "#f97316",
|
||||
bottom: "10%",
|
||||
left: "2%",
|
||||
delay: "1.5s",
|
||||
dur: "10s",
|
||||
},
|
||||
{
|
||||
size: 60,
|
||||
color: "#22c55e",
|
||||
top: "50%",
|
||||
right: "3%",
|
||||
delay: "0.8s",
|
||||
dur: "8s",
|
||||
},
|
||||
];
|
||||
|
||||
const INITIALS = ["JD", "AS", "MK", "RP", "LL"];
|
||||
|
||||
export const Register = () => {
|
||||
const [name, setName] = useState("");
|
||||
@ -214,12 +462,8 @@ export const Register = () => {
|
||||
const navigate = useNavigate();
|
||||
const { register, isLoading, error, clearError } = useAuthStore();
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
|
||||
const handleAvatarChange = (url: string) => {
|
||||
setAvatarUrl(url);
|
||||
setAvatarError(false);
|
||||
};
|
||||
const strength = getStrength(password);
|
||||
const isValid = name.trim() && email.trim() && password.length >= 6;
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
@ -233,97 +477,233 @@ export const Register = () => {
|
||||
if (success) navigate("/student/home", { replace: true });
|
||||
};
|
||||
|
||||
const isValid = name.trim() && email.trim() && password.length >= 6;
|
||||
|
||||
return (
|
||||
<div className="rg-screen">
|
||||
<div className="rg-root">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
{/* Blobs */}
|
||||
<div className="rg-blob rg-blob-1" />
|
||||
<div className="rg-blob rg-blob-2" />
|
||||
<div className="rg-blob rg-blob-3" />
|
||||
<div className="rg-blob rg-blob-4" />
|
||||
{/* ── LEFT PANEL ── */}
|
||||
<div className="rg-left">
|
||||
<div className="rg-panel-blob rg-panel-blob-1" />
|
||||
<div className="rg-panel-blob rg-panel-blob-2" />
|
||||
<div className="rg-panel-blob rg-panel-blob-3" />
|
||||
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
{/* Decorative floating dots */}
|
||||
{PANEL_DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rg-dot"
|
||||
className="rg-shape"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
borderRadius: "50%",
|
||||
opacity: 0.12,
|
||||
top: d.top,
|
||||
left: d.left,
|
||||
right: d.right,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: `${5.5 + i * 0.4}s`,
|
||||
animationDuration: d.dur,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="rg-card">
|
||||
{/* Logo + heading */}
|
||||
<div className="rg-logo-wrap">
|
||||
<img
|
||||
src="src/assets/ed_logo.png"
|
||||
alt="EdBridge"
|
||||
style={{
|
||||
width: 600,
|
||||
height: 70,
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
{/* Stars */}
|
||||
{[
|
||||
{ top: "14%", left: "20%", size: 14, delay: "0s" },
|
||||
{ top: "28%", left: "78%", size: 10, delay: "0.7s" },
|
||||
{ top: "52%", left: "30%", size: 12, delay: "1.3s" },
|
||||
{ top: "70%", left: "65%", size: 8, delay: "0.4s" },
|
||||
{ top: "88%", left: "22%", size: 10, delay: "1.8s" },
|
||||
].map((s, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className="rg-star"
|
||||
size={s.size}
|
||||
style={
|
||||
{
|
||||
top: s.top,
|
||||
left: s.left,
|
||||
animationDelay: s.delay,
|
||||
fill: "#fde68a",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Avatar preview */}
|
||||
{/* Decorative ring shapes */}
|
||||
{[
|
||||
{ size: 90, top: "72%", left: "5%", delay: "0.2s", dur: "10s" },
|
||||
{ size: 60, top: "8%", left: "55%", delay: "1.1s", dur: "13s" },
|
||||
].map((r, i) => (
|
||||
<div
|
||||
className={`rg-avatar-preview ${avatarUrl && !avatarError ? "has-image" : ""}`}
|
||||
key={i}
|
||||
className="rg-shape"
|
||||
style={
|
||||
{
|
||||
width: r.size,
|
||||
height: r.size,
|
||||
border: "2.5px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: "50%",
|
||||
top: r.top,
|
||||
left: r.left,
|
||||
animationDelay: r.delay,
|
||||
animationDuration: r.dur,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="rg-panel-content">
|
||||
{/* Logo */}
|
||||
<div className="rg-panel-logo">
|
||||
<div className="rg-panel-logo-badge">📚</div>
|
||||
<span className="rg-panel-logo-text">EdBridge</span>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<div className="rg-panel-headline">
|
||||
<h2>
|
||||
Ace the SAT.
|
||||
<br />
|
||||
<span>Start for free.</span>
|
||||
</h2>
|
||||
<p>
|
||||
Join thousands of students who improved their
|
||||
<br />
|
||||
SAT scores with personalized practice.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature pills */}
|
||||
<div className="rg-features">
|
||||
{FEATURES.map((f, i) => (
|
||||
<div className="rg-feature" key={i}>
|
||||
<div className="rg-feature-icon" style={{ background: f.bg }}>
|
||||
{f.icon}
|
||||
</div>
|
||||
<div className="rg-feature-text">
|
||||
<strong>{f.title}</strong>
|
||||
<span>{f.sub}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social proof */}
|
||||
<div className="rg-social-proof">
|
||||
<div className="rg-avatars">
|
||||
{INITIALS.map((s, i) => (
|
||||
<div className="rg-av" key={i}>
|
||||
{s}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>
|
||||
<strong>2,400+</strong> students already enrolled
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── RIGHT PANEL ── */}
|
||||
<div className="rg-right">
|
||||
{BG_DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rg-bg-dot"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
top: (d as any).top,
|
||||
right: (d as any).right,
|
||||
bottom: (d as any).bottom,
|
||||
left: (d as any).left,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: d.dur,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="rg-form-wrap">
|
||||
{/* Header */}
|
||||
<div className="rg-form-header">
|
||||
<h1>Create your account ✨</h1>
|
||||
<p>Fill in the details below to get started</p>
|
||||
</div>
|
||||
|
||||
{/* Avatar URL row */}
|
||||
<div className="rg-avatar-row">
|
||||
<div
|
||||
className={`rg-avatar-ring ${avatarUrl && !avatarError ? "filled" : ""}`}
|
||||
>
|
||||
{avatarUrl && !avatarError ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar preview"
|
||||
alt="Avatar"
|
||||
onError={() => setAvatarError(true)}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon size={22} color="#d1d5db" />
|
||||
<ImageIcon size={20} color="#d1d5db" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="rg-title">Create account ✨</h1>
|
||||
<p className="rg-sub">Join EdBridge and start your SAT prep</p>
|
||||
<div className="rg-avatar-input-col">
|
||||
<span className="rg-avatar-label">
|
||||
Avatar URL{" "}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
textTransform: "none",
|
||||
letterSpacing: 0,
|
||||
color: "#c4b5fd",
|
||||
fontSize: "0.68rem",
|
||||
}}
|
||||
>
|
||||
(optional)
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
className="rg-avatar-input"
|
||||
type="url"
|
||||
placeholder="https://example.com/photo.jpg"
|
||||
value={avatarUrl}
|
||||
onChange={(e) => {
|
||||
setAvatarUrl(e.target.value);
|
||||
setAvatarError(false);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<span className="rg-avatar-hint">
|
||||
Paste any image URL to set your profile photo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fields */}
|
||||
<div className="rg-fields">
|
||||
{/* Name */}
|
||||
{/* Name + Email row */}
|
||||
<div className="rg-row">
|
||||
<div className="rg-field">
|
||||
<label className="rg-label" htmlFor="name">
|
||||
Full Name
|
||||
</label>
|
||||
<div className="rg-input-wrap">
|
||||
<User size={15} className="rg-input-icon" />
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
className="rg-input"
|
||||
style={{ paddingLeft: "1rem" }}
|
||||
placeholder="Jane Doe"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="rg-field">
|
||||
@ -340,36 +720,6 @@ export const Register = () => {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar URL */}
|
||||
<div className="rg-field">
|
||||
<label className="rg-label" htmlFor="avatarUrl">
|
||||
Avatar URL{" "}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
textTransform: "none",
|
||||
letterSpacing: 0,
|
||||
color: "#c4b5fd",
|
||||
}}
|
||||
>
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<div className="rg-input-wrap">
|
||||
<ImageIcon size={15} className="rg-input-icon" />
|
||||
<input
|
||||
id="avatarUrl"
|
||||
type="url"
|
||||
className="rg-input"
|
||||
placeholder="https://example.com/photo.jpg"
|
||||
value={avatarUrl}
|
||||
onChange={(e) => handleAvatarChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -385,32 +735,24 @@ export const Register = () => {
|
||||
id="password"
|
||||
type="password"
|
||||
className="rg-input"
|
||||
placeholder="••••••••"
|
||||
placeholder="Min. 6 characters"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{/* Strength bar */}
|
||||
{password && (
|
||||
<>
|
||||
<div className="rg-strength-bar">
|
||||
{[1, 2, 3].map((seg) => (
|
||||
<div
|
||||
key={seg}
|
||||
className={`rg-strength-seg ${
|
||||
strength >= seg
|
||||
? `active-${STRENGTH_CLASSES[strength]}`
|
||||
: ""
|
||||
}`}
|
||||
className={`rg-strength-seg ${strength >= seg ? S_CLASS[strength] : ""}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className={`rg-strength-label ${STRENGTH_CLASSES[strength]}`}
|
||||
>
|
||||
{STRENGTH_LABELS[strength]} password
|
||||
<p className={`rg-strength-hint ${S_CLASS[strength]}`}>
|
||||
{S_LABEL[strength]} password
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@ -431,7 +773,8 @@ export const Register = () => {
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 size={18} className="rg-spinner" /> Creating account...
|
||||
<Loader2 size={18} className="rg-spinner" /> Creating
|
||||
account...
|
||||
</>
|
||||
) : (
|
||||
"Create Account →"
|
||||
@ -439,7 +782,7 @@ export const Register = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="rg-footer">
|
||||
<p className="rg-form-footer">
|
||||
Already have an account?{" "}
|
||||
<a href="/login" className="rg-link">
|
||||
Sign in
|
||||
@ -447,5 +790,6 @@ export const Register = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||
import { Home, BookOpen, Award, User, Video, Map } from "lucide-react";
|
||||
import { Home, BookOpen, Award, User, Map, SquareLibrary } from "lucide-react";
|
||||
import { SidebarProvider } from "../../components/ui/sidebar";
|
||||
import { AppSidebar } from "../../components/AppSidebar";
|
||||
|
||||
@ -27,7 +27,7 @@ const NAV_ITEMS = [
|
||||
},
|
||||
{
|
||||
to: "/student/lessons",
|
||||
icon: Video,
|
||||
icon: SquareLibrary,
|
||||
label: "Lessons",
|
||||
color: "#0891b2",
|
||||
bg: "rgba(8,145,178,0.12)",
|
||||
|
||||
1030
src/pages/student/lessons/EBRWBoundariesLesson.tsx
Normal file
1030
src/pages/student/lessons/EBRWBoundariesLesson.tsx
Normal file
File diff suppressed because it is too large
Load Diff
773
src/pages/student/lessons/EBRWCentralIdeasLesson.tsx
Normal file
773
src/pages/student/lessons/EBRWCentralIdeasLesson.tsx
Normal file
@ -0,0 +1,773 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Check, BookOpen, Lightbulb, Zap, Target } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
CENTRAL_IDEAS_EASY,
|
||||
CENTRAL_IDEAS_MEDIUM,
|
||||
} from "../../../data/rw/central-ideas-details";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const WRONG_ANSWER_TAXONOMY: RevealCard[] = [
|
||||
{
|
||||
label: "Off-topic",
|
||||
sublabel: "Is this noun/idea actually in the lines?",
|
||||
content:
|
||||
"Mentions something never in the passage or unrelated to the topic.",
|
||||
},
|
||||
{
|
||||
label: "Too broad",
|
||||
sublabel: "Does answer scope match passage scope?",
|
||||
content:
|
||||
'Passage focuses on ONE person but answer says "scientists" or "artists" (plural).',
|
||||
},
|
||||
{
|
||||
label: "Too narrow",
|
||||
sublabel: "Is this the MAIN thing or just one example?",
|
||||
content: "Picks a supporting detail instead of the overall idea.",
|
||||
},
|
||||
{
|
||||
label: "Half-right, half-wrong",
|
||||
sublabel: "Read every word in the answer.",
|
||||
content:
|
||||
"First half matches, second half contains a false or unsupported claim.",
|
||||
},
|
||||
{
|
||||
label: "Could-be-true",
|
||||
sublabel: "Can I point to specific words?",
|
||||
content:
|
||||
"Plausible in the real world, but not stated or implied in the text.",
|
||||
},
|
||||
{
|
||||
label: "Wrong scope for purpose",
|
||||
sublabel: "Focus on what the WHOLE passage does.",
|
||||
content: "Describes what the passage mentions but not its primary goal.",
|
||||
},
|
||||
];
|
||||
|
||||
const STRUCTURE_PATTERNS: RevealCard[] = [
|
||||
{
|
||||
label: "Old Idea → New Idea",
|
||||
content: "Challenge/revision: most common in science passages",
|
||||
},
|
||||
{
|
||||
label: "Problem → Solution",
|
||||
content: "A challenge is identified, then an approach is described",
|
||||
},
|
||||
{
|
||||
label: "Claim → Supporting Evidence",
|
||||
content: "The main point is stated upfront, followed by examples",
|
||||
},
|
||||
{
|
||||
label: "Description → Implication",
|
||||
content: "A scenario is described, then its significance is analyzed",
|
||||
},
|
||||
{
|
||||
label: "Comparison / Contrast",
|
||||
content: "Two entities or views are presented side by side",
|
||||
},
|
||||
];
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question: "Which sentence states the central idea of this passage?",
|
||||
passage: [
|
||||
"For decades, the standard treatment for depression has been antidepressant medication combined with talk therapy.",
|
||||
"These approaches help many patients, but roughly one-third do not respond to first-line treatments.",
|
||||
"Researchers are now investigating ketamine, an anesthetic, as a rapid-acting antidepressant.",
|
||||
"Unlike traditional medications that take weeks to work, ketamine can reduce depressive symptoms within hours.",
|
||||
"This suggests that the neuroscience of depression is far more complex — and far more treatable — than previously assumed.",
|
||||
],
|
||||
evidenceIndex: 4,
|
||||
explanation:
|
||||
'Sentence 5 is the "So What" — it draws a broader conclusion about what ketamine research implies about depression science. It\'s the central idea because it states what the author wants us to take away from all the preceding information.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Which sentence best expresses the main point the author wants the reader to understand?",
|
||||
passage: [
|
||||
"Ancient Rome is often praised for its engineering feats: aqueducts, roads, and amphitheaters.",
|
||||
"These structures have survived millennia and continue to function in some cases today.",
|
||||
"Less celebrated is Rome's sophisticated financial system, which included credit, interest-bearing loans, and transferable debt.",
|
||||
"Roman bankers financed trade across the Mediterranean, enabling commerce that would otherwise have been impossible.",
|
||||
"The financial innovations of Rome were as consequential as its physical ones, yet history has largely ignored them.",
|
||||
],
|
||||
evidenceIndex: 4,
|
||||
explanation:
|
||||
'Sentence 5 is the thesis — the author\'s main argument that Roman financial innovation was equally important to physical engineering. The word "yet" signals this is the key contrast and the point the author most wants to make.',
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWCentralIdeasLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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="Topic & Main Point"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Old/New & Structure"
|
||||
icon={Target}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={2}
|
||||
title="Pronouns & Compression"
|
||||
icon={Lightbulb}
|
||||
/>
|
||||
<SectionMarker index={3} title="Main Point Hunter" icon={Target} />
|
||||
<SectionMarker index={4} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* Section 0 — Topic & Main Point */}
|
||||
<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 w-fit">
|
||||
Information & Ideas — Domain 2
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Central Ideas & Details
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Identify the main point, central claim, and overall structure — and
|
||||
eliminate every wrong answer type.
|
||||
</p>
|
||||
|
||||
{/* 4A — Identifying the Topic */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
Identifying the Topic
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The topic is the person, thing, or idea that is the primary
|
||||
subject of the passage. Correct answers to main idea questions
|
||||
must reference the topic — wrong answers often shift to a related
|
||||
but different subject.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-700 text-sm mb-2">
|
||||
Key Principles
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>
|
||||
• The topic appears in sentence 1–2 in nearly every SAT
|
||||
passage.
|
||||
</li>
|
||||
<li>
|
||||
• A topic is NOT the same as the theme. Topic = subject;
|
||||
Theme = abstract lesson.
|
||||
</li>
|
||||
<li>
|
||||
• Recognize restatements: a computer → "the machine," "the
|
||||
invention," "the technology."
|
||||
</li>
|
||||
<li>
|
||||
• The topic word usually recurs throughout — by name or via
|
||||
pronoun/compression noun.
|
||||
</li>
|
||||
<li>
|
||||
• Off-topic answers are wrong even if every other word
|
||||
matches the passage.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-red-100 rounded-xl p-4">
|
||||
<p className="font-bold text-red-700 text-sm mb-2">
|
||||
Common Mistakes
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>
|
||||
• Naming a category instead of the specific topic (e.g.,
|
||||
"Japanese art" instead of "Otagaki Rengetsu's art").
|
||||
</li>
|
||||
<li>
|
||||
• Confusing a supporting detail mentioned in one sentence
|
||||
for the main topic.
|
||||
</li>
|
||||
<li>
|
||||
• Missing pronoun shifts: "it" may change referent
|
||||
mid-passage.
|
||||
</li>
|
||||
<li>
|
||||
• Selecting an answer that is too broad in scope — the
|
||||
passage discusses ONE scientist, not all scientists.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4B — Main Point Formula */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
The Main Point Formula
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The main point answers the question: "So what?" It is the primary
|
||||
argument the author wants to convey — not just a description of
|
||||
what was discussed.
|
||||
</p>
|
||||
<div className="scroll-reveal-scale bg-teal-900 text-white rounded-xl p-5 text-center">
|
||||
<p className="text-xl font-extrabold tracking-wide">
|
||||
Topic + So What? = Main Point
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Worked Example — Otagaki Rengetsu */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-5 space-y-3">
|
||||
<p className="font-bold text-teal-800 text-sm uppercase tracking-wider">
|
||||
Worked Example
|
||||
</p>
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 text-sm text-slate-700 leading-relaxed italic">
|
||||
"Admired primarily for her exquisite calligraphy,{" "}
|
||||
<span className="font-bold text-teal-700 not-italic">
|
||||
Otagaki Rengetsu
|
||||
</span>{" "}
|
||||
(1791–1875) was among Japan's most celebrated artists. She was
|
||||
also a writer and ceramicist, often inscribing{" "}
|
||||
<span className="underline decoration-teal-400">her poems</span>{" "}
|
||||
in{" "}
|
||||
<span className="underline decoration-teal-400">
|
||||
her own calligraphy
|
||||
</span>{" "}
|
||||
onto clay vessels —{" "}
|
||||
<span className="font-bold text-teal-700 not-italic">
|
||||
a distinctive blending of art forms not replicated by any
|
||||
other artist in Japanese history
|
||||
</span>
|
||||
.{" "}
|
||||
<span className="underline decoration-teal-400">Her work</span>{" "}
|
||||
was in such great demand during the nineteenth century that
|
||||
every household in Kyoto was said to own{" "}
|
||||
<span className="underline decoration-teal-400">
|
||||
her pottery
|
||||
</span>
|
||||
, and today{" "}
|
||||
<span className="underline decoration-teal-400">
|
||||
scrolls and ceramics
|
||||
</span>{" "}
|
||||
bearing{" "}
|
||||
<span className="underline decoration-teal-400">
|
||||
her calligraphy
|
||||
</span>{" "}
|
||||
are sought after by collectors."
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="card-tilt bg-white border border-teal-200 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">
|
||||
Topic
|
||||
</p>
|
||||
<p className="font-bold text-teal-800 text-sm">
|
||||
Otagaki Rengetsu's art
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-teal-200 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">
|
||||
So What?
|
||||
</p>
|
||||
<p className="font-bold text-teal-800 text-sm">
|
||||
Unique, unreplicated qualities
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-tilt bg-teal-700 text-white rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-teal-200 uppercase tracking-wider mb-1">
|
||||
Main Point
|
||||
</p>
|
||||
<p className="font-bold text-sm">
|
||||
Her artistic creations are prized for their unique qualities
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-xs text-slate-700">
|
||||
<span className="font-bold text-amber-800">
|
||||
Notice the topic tracking:
|
||||
</span>{" "}
|
||||
The topic "Otagaki Rengetsu's art" is introduced in sentence 1
|
||||
and then restated in other words throughout —{" "}
|
||||
<em>
|
||||
her poems, her own calligraphy, her work, her pottery,
|
||||
scrolls and ceramics, her calligraphy
|
||||
</em>
|
||||
. The topic word recurs by name or via pronoun/compression
|
||||
noun. Every correct answer must reference this specific topic,
|
||||
not a broader category like "Japanese art."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-600 font-semibold">
|
||||
Key locations where the main point is typically found:
|
||||
</p>
|
||||
<ul className="space-y-1 text-sm text-slate-700">
|
||||
<li>
|
||||
•{" "}
|
||||
<span className="font-bold">First or first two sentences</span>{" "}
|
||||
— most common.
|
||||
</li>
|
||||
<li>
|
||||
• <span className="font-bold">Last sentence</span> — especially
|
||||
when the passage confirms or reasserts the opening claim.
|
||||
</li>
|
||||
<li>
|
||||
• <span className="font-bold">After a major transition</span>{" "}
|
||||
such as however, but, in fact, therefore — the "new idea" is
|
||||
often the real main point.
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<span className="font-bold">
|
||||
After a dash, colon, or italicized word
|
||||
</span>{" "}
|
||||
— these signal that something important follows.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Fiction & Poetry */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-slate-50 border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Fiction & Poetry: Special Cases
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="font-bold text-teal-700 text-sm mb-1">
|
||||
Fiction Passages
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• Focus on who the character is and what quality or feeling
|
||||
the passage emphasizes.
|
||||
</li>
|
||||
<li>
|
||||
• Key information often appears at the end (a quoted line of
|
||||
dialogue, a narrator's summary, or a character's
|
||||
reflection).
|
||||
</li>
|
||||
<li>
|
||||
• DO NOT read in symbolism or broader themes beyond what the
|
||||
text literally states.
|
||||
</li>
|
||||
<li>
|
||||
• Wrong answers often import an emotion or quality from
|
||||
outside the text, or conflate a detail with the main idea.
|
||||
</li>
|
||||
<li className="italic text-slate-400">
|
||||
Example: Amy Tan passage about ink-making → main point =
|
||||
characters take great pride in their generational ink-making
|
||||
tradition. NOT "the importance of family" (too abstract).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-teal-700 text-sm mb-1">
|
||||
Poetry Passages
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• Read the poem literally first — what is the speaker
|
||||
literally saying?
|
||||
</li>
|
||||
<li>
|
||||
• Identify the speaker's attitude (positive, negative,
|
||||
ambivalent) — this almost always determines the main point.
|
||||
</li>
|
||||
<li>
|
||||
• Look at the last stanza or final couplet for the poem's
|
||||
culminating idea.
|
||||
</li>
|
||||
<li>
|
||||
• Avoid symbolic overreach: "the winter represents death" is
|
||||
interpretation, not literal reading.
|
||||
</li>
|
||||
<li>
|
||||
• Process of elimination is very powerful: eliminate any
|
||||
answer that is either negative when the poem is positive, or
|
||||
that mentions something never stated.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4F — Primary Purpose vs. Main Point */}
|
||||
<div className="scroll-reveal stagger-4 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
Primary Purpose vs. Main Point
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-teal-600 text-white">
|
||||
<th className="px-4 py-2 text-left"></th>
|
||||
<th className="px-4 py-2 text-left">Main Point</th>
|
||||
<th className="px-4 py-2 text-left">Primary Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
[
|
||||
"Question asked",
|
||||
"What does the author claim?",
|
||||
"Why did the author write this?",
|
||||
],
|
||||
[
|
||||
"Answer uses",
|
||||
'Specific nouns, claims, findings ("dark matter cannot be seen but must exist")',
|
||||
"Function verbs: describe, argue, challenge, illustrate, explain, contrast, suggest",
|
||||
],
|
||||
[
|
||||
"Example",
|
||||
'"Octavia Butler resisted being identified exclusively with science fiction."',
|
||||
'"To present a claim and support it with examples."',
|
||||
],
|
||||
].map(([label, mp, pp], i) => (
|
||||
<tr
|
||||
key={label}
|
||||
className={i % 2 === 0 ? "bg-white" : "bg-teal-50"}
|
||||
>
|
||||
<td className="px-4 py-2 font-bold text-slate-700 text-xs">
|
||||
{label}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-600 text-xs">{mp}</td>
|
||||
<td className="px-4 py-2 text-slate-600 text-xs">{pp}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wrong Answer Taxonomy */}
|
||||
<div className="scroll-reveal stagger-5 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Wrong Answer Taxonomy — tap to reveal each trap:
|
||||
</h3>
|
||||
<RevealCardGrid
|
||||
cards={WRONG_ANSWER_TAXONOMY}
|
||||
columns={3}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 1 — Old/New & Structure */}
|
||||
<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">
|
||||
Old/New & Structure
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
The most important structural pattern on the SAT — and how to
|
||||
describe overall passage organization.
|
||||
</p>
|
||||
|
||||
{/* 4C — Old/New */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
Old Idea vs. New Idea Structure
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The Old/New template is one of the most important patterns in SAT
|
||||
science and social science passages. Authors present a
|
||||
traditionally held view (old idea), then pivot to a new or
|
||||
contradictory finding.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-700 text-sm mb-2">
|
||||
Old Idea Signal Phrases
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• "Some/many/most scientists believe..."</li>
|
||||
<li>• "It is commonly thought that..."</li>
|
||||
<li>• "Accepted/conventional wisdom holds..."</li>
|
||||
<li>• "For decades, researchers thought..."</li>
|
||||
<li>• "Traditionally, it was believed..."</li>
|
||||
</ul>
|
||||
<p className="text-xs text-red-600 mt-2 italic">
|
||||
→ These phrases signal a view the author DISAGREES with.
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-green-700 text-sm mb-2">
|
||||
New Idea Signal Phrases
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• "However, but in fact..."</li>
|
||||
<li>• "Actually, in reality..."</li>
|
||||
<li>• "But is it really true that...?"</li>
|
||||
<li>• "It now seems / researchers now think..."</li>
|
||||
<li>• "Recently, it has been found that..."</li>
|
||||
<li>• "New research/evidence shows..."</li>
|
||||
</ul>
|
||||
<p className="text-xs text-green-600 mt-2 italic">
|
||||
→ These phrases signal the view the author AGREES with.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-teal-900 text-white rounded-xl p-4">
|
||||
<p className="font-bold text-sm mb-1">STRATEGY</p>
|
||||
<p className="text-xs text-teal-100">
|
||||
As you read, jot on scratch paper: Old = [3-word summary] | New
|
||||
= [3-word summary]. The main point is almost always the NEW
|
||||
idea. If you identify the old idea, you can predict the new idea
|
||||
before reading it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4G — Overall Structure */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Overall Structure of a Text — tap to reveal each pattern:
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Structure questions ask how a passage is organized. Identify the
|
||||
move from one idea to another — not just what is said, but in what
|
||||
sequence and for what purpose.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={STRUCTURE_PATTERNS}
|
||||
columns={3}
|
||||
accentColor="teal"
|
||||
/>
|
||||
<div className="bg-slate-800 text-white rounded-xl p-4">
|
||||
<p className="font-bold text-sm mb-1">
|
||||
STRATEGY for structure questions
|
||||
</p>
|
||||
<p className="text-xs text-slate-200">
|
||||
Focus on the first and last sentence. The first usually
|
||||
introduces the main move; the last usually shows where the
|
||||
passage ended up. Then check the answer choices for the option
|
||||
that correctly names both ends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 2 — Pronouns & Compression Nouns */}
|
||||
<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-2">
|
||||
Pronouns & Compression Nouns
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Failure to track these referents is one of the most common
|
||||
comprehension errors on the SAT.
|
||||
</p>
|
||||
|
||||
{/* 4H */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
Tracking Pronouns and Compression Nouns
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
Pronouns (it, they, this, these) and "compression nouns" (this
|
||||
phenomenon, the former, such developments) refer to ideas already
|
||||
stated. Failure to track these referents is one of the most common
|
||||
comprehension errors on the SAT.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-700 text-sm mb-2">
|
||||
How to Track Pronouns
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• Always back up to the previous sentence (or earlier) to
|
||||
find the referent.
|
||||
</li>
|
||||
<li>• Singular pronouns (it, this) → singular noun.</li>
|
||||
<li>• Plural pronouns (they, these) → plural noun.</li>
|
||||
<li>
|
||||
• "The former" → first-mentioned item; "the latter" →
|
||||
second-mentioned item.
|
||||
</li>
|
||||
<li>
|
||||
• Do NOT start reading at the pronoun; always work backwards
|
||||
first.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-700 text-sm mb-2">
|
||||
Compression Noun Examples
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• "This enhanced convenience" → refers to several sentences
|
||||
of prior description.
|
||||
</li>
|
||||
<li>
|
||||
• "This divergence" → refers to genetic differences between
|
||||
dolphin species.
|
||||
</li>
|
||||
<li>
|
||||
• "Such developments" → refers to a process described in the
|
||||
prior paragraph.
|
||||
</li>
|
||||
<li>
|
||||
• "This phenomenon" → compresses a multi-sentence
|
||||
explanation into one noun.
|
||||
</li>
|
||||
<li>
|
||||
• The referent may be 3–5 lines BEFORE the compression noun.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-teal-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-teal-100">
|
||||
The main point is NOT the first sentence — it is the "So What"
|
||||
conclusion. Read the whole passage, then ask: "If I had to explain
|
||||
in one sentence what the author WANTS me to believe, what would I
|
||||
say?" That sentence is the main point. For Old/New passages: the
|
||||
NEW idea is almost always the main point.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 3 — Main Point Hunter widget */}
|
||||
<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-2">
|
||||
Main Point Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Find the sentence that states the central idea — the "So What"
|
||||
conclusion.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Section 4 — Practice */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</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 EBRWCentralIdeasLesson;
|
||||
624
src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx
Normal file
624
src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx
Normal file
@ -0,0 +1,624 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Check, BookOpen, BarChart3, Zap, Target } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
COMMAND_EVIDENCE_EASY,
|
||||
COMMAND_EVIDENCE_MEDIUM,
|
||||
} from "../../../data/rw/command-of-evidence";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const ILLUSTRATION_TRAPS: RevealCard[] = [
|
||||
{
|
||||
label: "Wrong speaker",
|
||||
content:
|
||||
"The quotation features the correct idea but from a different character.",
|
||||
},
|
||||
{
|
||||
label: "Right topic, wrong direction",
|
||||
content:
|
||||
"The quotation mentions the topic but doesn't illustrate the specific claim.",
|
||||
},
|
||||
{
|
||||
label: "Too indirect",
|
||||
content:
|
||||
"The connection between quotation and claim requires too much inferential leaping.",
|
||||
},
|
||||
{
|
||||
label: "Question marks",
|
||||
content: 'A rhetorical question often cannot "illustrate" a direct claim.',
|
||||
},
|
||||
];
|
||||
|
||||
const VALIDITY_TYPES: RevealCard[] = [
|
||||
{
|
||||
label: "Valid / Necessary",
|
||||
sublabel: "CORRECT on SAT",
|
||||
content:
|
||||
"Must be true given the evidence; the only logical conclusion. Example: If 14% are supershear events → 86% are not.",
|
||||
},
|
||||
{
|
||||
label: "Possible / Speculative",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
'Might be true but the evidence doesn\'t require it. Example: "Researchers must want more funding" — not stated.',
|
||||
},
|
||||
{
|
||||
label: "Contradicted",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
'Directly conflicts with information stated in the passage. Example: "Exercise improves fitness equally for all" — passage says otherwise.',
|
||||
},
|
||||
{
|
||||
label: "Off-topic",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
"No logical connection to the claim or evidence. Example: Ocean temperature claim when passage is about land volcanoes.",
|
||||
},
|
||||
];
|
||||
|
||||
const QUANT_WRONG_ANSWERS: RevealCard[] = [
|
||||
{
|
||||
label: "Wrong subgroup / time period",
|
||||
content: "Accurate data about the WRONG subgroup or time period.",
|
||||
},
|
||||
{
|
||||
label: "Wrong direction",
|
||||
content:
|
||||
"Accurate comparison in the WRONG direction (A > B when claim needs B > A).",
|
||||
},
|
||||
{
|
||||
label: "Wrong number of groups",
|
||||
content: "Involves TWO groups when the claim is about ONE group only.",
|
||||
},
|
||||
{
|
||||
label: "Contradictory trend",
|
||||
content:
|
||||
"Describes a trend that contradicts the claim despite accurate numbers.",
|
||||
},
|
||||
{
|
||||
label: "Right data, wrong claim",
|
||||
content:
|
||||
"Describes the graph accurately but doesn't address the specific claim.",
|
||||
},
|
||||
];
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"The researcher concludes that urban green spaces reduce stress. Which sentence from the study best SUPPORTS this conclusion?",
|
||||
passage: [
|
||||
"Participants were randomly assigned to walk for 30 minutes in either an urban park or a busy commercial district.",
|
||||
"Before and after each walk, cortisol levels were measured using saliva samples.",
|
||||
"Participants who walked in the park showed a 15% reduction in cortisol, a primary stress hormone.",
|
||||
"Those who walked in the commercial district showed no significant change in cortisol levels.",
|
||||
"Participants reported feeling calmer after the park walk, though self-report data is inherently subjective.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 provides direct biological evidence (cortisol reduction) that supports the claim about stress reduction. It uses objective measurement rather than self-report, making it the strongest support for the stated conclusion.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Which sentence from this passage most effectively ILLUSTRATES the claim that microplastics are now found in unexpected locations?",
|
||||
passage: [
|
||||
"Microplastics are plastic fragments smaller than 5 millimeters.",
|
||||
"They originate from the breakdown of larger plastic items or are manufactured at microscopic size.",
|
||||
"Researchers have detected microplastics in the peak snowpack of Mount Everest.",
|
||||
"Microplastics have also been found in human blood, lung tissue, and placentas.",
|
||||
"The long-term health effects of microplastic exposure are still being studied.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 best illustrates the claim about unexpected locations because Mount Everest is one of the most remote places on Earth — finding microplastics there is a striking, concrete example of how pervasive contamination has become.",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWCommandEvidenceLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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="Textual Evidence" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Quantitative Evidence"
|
||||
icon={BarChart3}
|
||||
/>
|
||||
<SectionMarker index={2} title="Evidence Hunter" icon={Target} />
|
||||
<SectionMarker index={3} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* Section 0 — Textual Evidence */}
|
||||
<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 w-fit">
|
||||
Information & Ideas — Domain 2
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Command of Evidence
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Move beyond the passage to apply its ideas. Two subtypes: Textual
|
||||
Evidence (quotations) and Quantitative Evidence (graphs and tables).
|
||||
</p>
|
||||
|
||||
<div className="scroll-reveal stagger-1 bg-teal-50 border border-teal-200 rounded-2xl p-5 mb-8">
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-bold text-teal-800">Overview: </span>Command
|
||||
of Evidence questions ask you to move BEYOND the passage to apply
|
||||
its ideas. You will identify quotations or data that illustrate,
|
||||
support, or undermine a specific claim. There are two main
|
||||
subtypes: Textual Evidence (using quotations or passages) and
|
||||
Quantitative Evidence (using graphs and tables).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 5A */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Illustrating a Claim (Quotation Selection)
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
These questions ask you to find the quotation from a poem, story,
|
||||
or passage that best illustrates a claim stated in the question
|
||||
stem. The claim is explicitly given to you — your job is to match
|
||||
it to the correct quotation.
|
||||
</p>
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
3-Step Process for Illustration Questions
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
[
|
||||
"1",
|
||||
"RESTATE the claim in the question stem in your own words. Identify the exact quality or action it describes.",
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"PREDICT what kind of language would illustrate it — positive/negative tone, specific action, direct statement?",
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"ELIMINATE quotations that: (a) are too vague, (b) refer to the wrong speaker, (c) describe a different quality entirely.",
|
||||
],
|
||||
].map(([n, text]) => (
|
||||
<div key={n} className="flex gap-2">
|
||||
<span className="w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{n}
|
||||
</span>
|
||||
<p className="text-xs text-slate-700">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-slate-800">
|
||||
Key traps in illustration questions — tap to reveal:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={ILLUSTRATION_TRAPS}
|
||||
columns={2}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 5B */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Supporting a Claim
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Support questions ask: "Which finding would MOST DIRECTLY support
|
||||
this conclusion?" The correct answer must provide new evidence
|
||||
consistent with the claim — it doesn't just repeat what the
|
||||
passage already states.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<p className="font-bold text-green-800 text-sm mb-2">
|
||||
What Makes a Valid Support?
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• Provides a NEW example or finding, not a restatement.
|
||||
</li>
|
||||
<li>
|
||||
• Is directly consistent with the specific mechanism
|
||||
described.
|
||||
</li>
|
||||
<li>• Makes the claim MORE likely to be true.</li>
|
||||
<li>
|
||||
• Common patterns: X causes Y → new example of X causing Y;
|
||||
More X → more Y → find a case where less X → less Y.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-2">
|
||||
What Looks Like Support But Isn't
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• The answer discusses the right topic but a different
|
||||
aspect of it.
|
||||
</li>
|
||||
<li>
|
||||
• The answer is consistent with the general field but not
|
||||
the specific claim.
|
||||
</li>
|
||||
<li>
|
||||
• The answer only restates part of what the passage already
|
||||
said.
|
||||
</li>
|
||||
<li>
|
||||
• The answer is factually true but would also be true
|
||||
regardless of the claim.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5C */}
|
||||
<div className="scroll-reveal stagger-4 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Undermining a Claim
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Undermine questions have the same structure as support questions,
|
||||
but in reverse. The correct answer must provide information that
|
||||
makes the claim LESS likely to be true.
|
||||
</p>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
KEY TECHNIQUE — Flip the Claim
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
If the claim is "high metabolic rate = survival advantage," then
|
||||
to undermine it you need evidence that high metabolic rate does
|
||||
NOT produce survival advantage (e.g., many high-metabolic
|
||||
creatures went extinct).
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-slate-800">
|
||||
Common undermine traps:
|
||||
</p>
|
||||
{[
|
||||
"The answer is unrelated to the claim rather than contradictory to it — an unrelated finding doesn't undermine anything.",
|
||||
"The answer challenges a secondary detail, not the core mechanism being tested.",
|
||||
"The answer actually supports the claim but is framed in negative-sounding language.",
|
||||
].map((trap, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-2 bg-red-50 border border-red-100 rounded-lg px-3 py-2"
|
||||
>
|
||||
<span className="text-red-500 font-bold shrink-0 text-xs">
|
||||
✗
|
||||
</span>
|
||||
<p className="text-xs text-slate-600">{trap}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 5D */}
|
||||
<div className="scroll-reveal stagger-5 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Validity of Conclusions — tap to reveal each type:
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Some questions ask whether a finding is valid — whether it
|
||||
necessarily follows from the research described.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={VALIDITY_TYPES}
|
||||
columns={2}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-teal-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule — Textual Evidence</p>
|
||||
<p className="text-sm text-teal-100">
|
||||
The question always tells you the required relationship
|
||||
(illustrate / support / undermine). An answer that accurately
|
||||
quotes the passage but has the WRONG relationship is still wrong.
|
||||
Identify the relationship first, accuracy second.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 1 — Quantitative Evidence */}
|
||||
<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">
|
||||
Quantitative Evidence
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Graphs and tables — the mandatory order of operations and criteria
|
||||
matching technique.
|
||||
</p>
|
||||
|
||||
{/* 5E */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-amber-50 border border-amber-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-amber-900">
|
||||
Non-Negotiable Order of Operations
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The most important principle:{" "}
|
||||
<strong>the graphic alone is never sufficient.</strong> You must
|
||||
begin with the passage and the question to know what to look for
|
||||
in the graphic.
|
||||
</p>
|
||||
<div className="bg-white border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-3">
|
||||
MANDATORY Order for Graph Questions
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
[
|
||||
"1",
|
||||
"Read the PASSAGE (especially the last sentence — this states the claim).",
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"Read the QUESTION STEM carefully to identify exactly what you are being asked.",
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"Extract the CRITERIA from the claim: what specific conditions must be met?",
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"THEN look at the GRAPHIC with those criteria in mind.",
|
||||
],
|
||||
[
|
||||
"5",
|
||||
"Match the answer that satisfies ALL criteria — not just part of them.",
|
||||
],
|
||||
].map(([n, text]) => (
|
||||
<div key={n} className="flex gap-2">
|
||||
<span className="w-5 h-5 rounded-full bg-amber-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{n}
|
||||
</span>
|
||||
<p className="text-xs text-slate-700">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-1">
|
||||
CRITICAL WARNING
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
Looking at the graph first is one of the most costly errors on
|
||||
graph questions. Multiple answer choices will accurately
|
||||
describe the graph — only ONE will match the specific claim in
|
||||
the passage. The graphic alone cannot tell you which one is
|
||||
correct.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5F */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-3">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
When You Do NOT Need the Graph
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Many graph questions can be answered using only the passage and
|
||||
the answer choices — without looking at the graph at all.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-slate-700">
|
||||
<li className="flex gap-2">
|
||||
<span className="text-teal-600 font-bold shrink-0">•</span>If
|
||||
answer choices contain wording clearly inconsistent with the
|
||||
passage's claim, eliminate them immediately.
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="text-teal-600 font-bold shrink-0">•</span>
|
||||
Answers addressing the wrong aspect of the claim (wrong time
|
||||
period, wrong variable, wrong group) can be eliminated before
|
||||
consulting the graph.
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="text-teal-600 font-bold shrink-0">•</span>Once
|
||||
only one answer remains that is consistent with the claim, that
|
||||
is correct — verifying against the graph is optional.
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-xs mb-1">EXAMPLE</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
Passage claims "print books are preferred in certain
|
||||
situations." Any answer describing a situation where e-books are
|
||||
preferred can be immediately eliminated — without looking at the
|
||||
chart.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5G */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Criteria Matching for Quantitative Questions
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
The most common error: choosing an answer that accurately
|
||||
describes the graph but fails to match ALL criteria specified in
|
||||
the claim. Build a checklist before looking at the data.
|
||||
</p>
|
||||
<div className="card-tilt bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
Building a Criteria Checklist
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• List every specific condition mentioned in the claim.</li>
|
||||
<li>
|
||||
• Example: "rebounded AND reached highest level in 60 years" =
|
||||
2 separate criteria.
|
||||
</li>
|
||||
<li>
|
||||
• An answer meeting only Criterion 1 but not Criterion 2 is
|
||||
wrong, even if it accurately describes the graph.
|
||||
</li>
|
||||
<li>
|
||||
• Write criteria on scratch paper before looking at graphic.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-slate-800">
|
||||
Common Quantitative Wrong Answers — tap to reveal:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={QUANT_WRONG_ANSWERS}
|
||||
columns={3}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-teal-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">
|
||||
Golden Rule — Quantitative Evidence
|
||||
</p>
|
||||
<p className="text-sm text-teal-100">
|
||||
Always read the passage and question first to extract criteria.
|
||||
Multiple answer choices will accurately describe the graph — only
|
||||
ONE matches the specific claim. "Right data, wrong claim aspect"
|
||||
is the most common wrong answer type.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 2 — Evidence Hunter widget */}
|
||||
<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-2">
|
||||
Evidence Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Find the sentence that has the exact relationship the question
|
||||
requires.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Section 3 — Practice */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</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 EBRWCommandEvidenceLesson;
|
||||
@ -239,7 +239,7 @@ const EBRWCommasLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
@ -117,7 +117,7 @@ const EBRWCraftStructureLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
496
src/pages/student/lessons/EBRWCrossTextLesson.tsx
Normal file
496
src/pages/student/lessons/EBRWCrossTextLesson.tsx
Normal file
@ -0,0 +1,496 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Check, BookOpen, Lightbulb, Zap, Target } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
CROSS_TEXT_EASY,
|
||||
CROSS_TEXT_MEDIUM,
|
||||
} from "../../../data/rw/cross-text-connections";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const RESPONSE_PATTERNS: RevealCard[] = [
|
||||
{
|
||||
label: "Overstated / exaggerated",
|
||||
content: '"It overstates the environmental impact of X."',
|
||||
},
|
||||
{
|
||||
label: "Insufficiently supported",
|
||||
content: '"The data do not support the conclusion drawn."',
|
||||
},
|
||||
{
|
||||
label: "Incomplete / overlooks evidence",
|
||||
content: '"It overlooks a crucial statistic / factor."',
|
||||
},
|
||||
{
|
||||
label: "Logically flawed",
|
||||
content: '"The reasoning does not follow from the evidence presented."',
|
||||
},
|
||||
];
|
||||
|
||||
const SCOPE_ERRORS: RevealCard[] = [
|
||||
{
|
||||
label: "Scope Error",
|
||||
content:
|
||||
"Answer is more general or more specific than what either text actually argues. The correct answer matches the EXACT scope of the relevant text.",
|
||||
},
|
||||
{
|
||||
label: "One-Text Error",
|
||||
content:
|
||||
'Answer accurately reflects one text but ignores the other. In "both agree" questions, every word must apply to both passages.',
|
||||
},
|
||||
{
|
||||
label: "Extreme Language",
|
||||
content:
|
||||
'Answer uses "always," "never," "all," "none" when neither author makes such an absolute claim.',
|
||||
},
|
||||
{
|
||||
label: "Fabricated Connection",
|
||||
content:
|
||||
"Answer invents a relationship between the texts that neither author states or implies.",
|
||||
},
|
||||
];
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"Text 1 claims social media increases political polarization. Which sentence from Text 2 most directly responds to this claim?",
|
||||
passage: [
|
||||
"Social media platforms are designed to maximize engagement through outrage.",
|
||||
"Studies of Twitter and Facebook show users cluster in ideological echo chambers.",
|
||||
"However, a 2022 meta-analysis of 31 studies found no consistent causal link between social media use and polarization.",
|
||||
"The authors note that correlation between social media use and polarized views may reflect self-selection bias.",
|
||||
"People who are already polarized may simply use social media more frequently.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
'Sentence 3 directly challenges the claim that social media causes polarization by citing a meta-analysis finding no consistent causal link. The word "however" signals this is the point where Text 2 diverges from Text 1\'s argument.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Both passages discuss the role of apprenticeships in education. Which sentence from Text 2 would Text 1's author most likely agree with?",
|
||||
passage: [
|
||||
"Modern universities emphasize abstract theory over practical skills.",
|
||||
"Employers increasingly report that graduates cannot apply classroom knowledge to real problems.",
|
||||
"Apprenticeship programs combine theoretical learning with supervised hands-on practice.",
|
||||
"Countries with strong apprenticeship traditions, such as Germany, report lower youth unemployment.",
|
||||
"Critics argue that apprenticeships limit social mobility by tracking students into specific trades.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
explanation:
|
||||
"Sentence 3 describes apprenticeships as combining theory and practice — this is the bridge both texts share. Text 1 likely argues for integrating practical skills with theoretical knowledge, making this the point of agreement.",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWCrossTextLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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="Paired Passage Strategy"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Relationship Types"
|
||||
icon={Lightbulb}
|
||||
/>
|
||||
<SectionMarker index={2} title="Connection Finder" icon={Target} />
|
||||
<SectionMarker index={3} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* ── SECTION 0: PAIRED PASSAGE 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-fuchsia-100 text-fuchsia-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4 w-fit">
|
||||
Craft & Structure — Domain 1
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Cross-Text Connections
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Two short passages, one question — always about the relationship
|
||||
between the authors' claims. Summarize each text before reading the
|
||||
question.
|
||||
</p>
|
||||
|
||||
{/* Format & Question Stems */}
|
||||
<div className="scroll-reveal stagger-1 bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Format & Common Question Stems
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
What to Expect
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Both texts are very short — typically 50–100 words each.",
|
||||
"They cover the same general topic but present different perspectives.",
|
||||
"Only one question accompanies the pair.",
|
||||
"The most common relationship is disagreement — one challenges or qualifies the other.",
|
||||
"Authors may also partially agree, or one may complicate the other's conclusion.",
|
||||
].map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-start gap-2 text-xs text-slate-700"
|
||||
>
|
||||
<span className="text-fuchsia-500 shrink-0 mt-0.5">
|
||||
•
|
||||
</span>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Common Question Stems
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
'"Based on the texts, how would [Author 2] most likely describe the view in Text 1?"',
|
||||
'"Based on the texts, how would [Author 2] most likely respond to what [Author 1] says about X?"',
|
||||
'"Both texts discuss X. How do they differ in their treatment of it?"',
|
||||
'"Which statement would [Author 1] most likely agree with?"',
|
||||
].map((stem) => (
|
||||
<li
|
||||
key={stem}
|
||||
className="text-xs text-slate-600 italic bg-fuchsia-50 rounded-lg px-2 py-1"
|
||||
>
|
||||
{stem}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Five-Step Process */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
The Five-Step Process
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
action: "Read Text 1 and identify its main point",
|
||||
tip: "Write it down in 4–6 words on scratch paper.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Read Text 2 and identify its main point",
|
||||
tip: "Write it down in 4–6 words on scratch paper.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Write the relationship between the two",
|
||||
tip: "Disagree, partially agree, or agree with different emphasis.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action:
|
||||
"Answer the question in your own words before looking at options",
|
||||
tip: "Base this on your written main points and the relationship.",
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action:
|
||||
"Read the answer choices and select the closest match",
|
||||
tip: "Use elimination — wrong charges can often be spotted from the first few words of each option.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.step}
|
||||
className="flex gap-3 bg-fuchsia-50 border border-fuchsia-100 rounded-xl p-4"
|
||||
>
|
||||
<span className="w-7 h-7 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-sm font-bold shrink-0">
|
||||
{s.step}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 text-sm">
|
||||
{s.action}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{s.tip}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-1">
|
||||
Critical Warning
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
The biggest mistake is reading both passages without writing
|
||||
anything down and relying on memory. Under exam pressure, this
|
||||
almost always leads to error. The notes take ten seconds and can
|
||||
save a minute of confusion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 1: RELATIONSHIP TYPES & ELIMINATION ── */}
|
||||
<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">
|
||||
Relationship Types
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Once the relationship is established, elimination becomes powerful —
|
||||
wrong-charge answers can be ruled out immediately.
|
||||
</p>
|
||||
|
||||
{/* 3 Relationship Types */}
|
||||
<div className="scroll-reveal stagger-1 bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
The 3 Relationship Types & How to Eliminate
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
type: "Disagree",
|
||||
color: "bg-red-50 border-red-200",
|
||||
badge: "bg-red-600",
|
||||
desc: "Authors have conflicting views on the same topic. Text 1 argues X; Text 2 argues the opposite or Not-X.",
|
||||
elim: 'Eliminate any answer that is positive (e.g., "strongly supported," "generally accurate") or neutral to the point of meaninglessness.',
|
||||
},
|
||||
{
|
||||
type: "Partially Agree",
|
||||
color: "bg-amber-50 border-amber-200",
|
||||
badge: "bg-amber-600",
|
||||
desc: "Authors share common ground on some aspects but diverge on others.",
|
||||
elim: "Eliminate answers that treat the relationship as purely positive or purely negative — the correct answer acknowledges both what they share and where they differ.",
|
||||
},
|
||||
{
|
||||
type: "Agree",
|
||||
color: "bg-green-50 border-green-200",
|
||||
badge: "bg-green-600",
|
||||
desc: "Authors reach the same basic conclusion, though they may approach it differently or emphasize different aspects.",
|
||||
elim: "Eliminate answers that overstate the disagreement, or that are accurate for one text but not both.",
|
||||
},
|
||||
].map((r) => (
|
||||
<div
|
||||
key={r.type}
|
||||
className={`card-tilt border rounded-xl p-4 ${r.color}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
className={`px-3 py-0.5 rounded-full text-white text-xs font-bold ${r.badge}`}
|
||||
>
|
||||
{r.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mb-1">{r.desc}</p>
|
||||
<p className="text-xs text-slate-500 italic">
|
||||
<span className="font-bold not-italic text-slate-600">
|
||||
To eliminate:{" "}
|
||||
</span>
|
||||
{r.elim}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Words to Watch + Response Patterns */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Words to Watch & Typical Response Patterns
|
||||
</h3>
|
||||
<div className="space-y-2 mb-4">
|
||||
<p className="text-sm text-slate-600">
|
||||
These words in answer choices typically signal incorrect
|
||||
answers:
|
||||
</p>
|
||||
{[
|
||||
[
|
||||
'"strongly supported," "proven," "definitively"',
|
||||
"Too positive / too absolute",
|
||||
],
|
||||
['"only possible in," "impossible to"', "Too extreme"],
|
||||
['"highly implausible"', "Usually overstates the disagreement"],
|
||||
].map(([phrase, note]) => (
|
||||
<div
|
||||
key={phrase}
|
||||
className="flex gap-3 bg-red-50 border border-red-200 rounded-lg px-4 py-2"
|
||||
>
|
||||
<span className="text-red-500 font-bold shrink-0">✗</span>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-red-800 italic">
|
||||
{phrase}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">{note}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-slate-700 mb-2">
|
||||
Typical Response Patterns — tap to reveal example phrasing:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={RESPONSE_PATTERNS}
|
||||
columns={2}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scope & Specificity Errors */}
|
||||
<div className="scroll-reveal stagger-3 bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Scope & Specificity Errors — tap to reveal each trap:
|
||||
</h3>
|
||||
<RevealCardGrid
|
||||
cards={SCOPE_ERRORS}
|
||||
columns={2}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-fuchsia-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-fuchsia-100">
|
||||
Summarize each text in one sentence BEFORE reading the question.
|
||||
Write it down — do not rely on memory. The question will always be
|
||||
about the relationship between those two summaries. If you know
|
||||
what each author claims and how they relate, the question becomes
|
||||
straightforward.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 2: WIDGET ── */}
|
||||
<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-2">
|
||||
Connection Finder
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Find the sentence that directly engages with the other text's
|
||||
argument.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 3: PRACTICE ── */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</h2>
|
||||
{CROSS_TEXT_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="fuchsia" />
|
||||
))}
|
||||
{CROSS_TEXT_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 EBRWCrossTextLesson;
|
||||
@ -225,7 +225,7 @@ const EBRWDashesApostrophesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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}
|
||||
|
||||
@ -119,7 +119,7 @@ const EBRWExplicitMeaningLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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}
|
||||
|
||||
@ -119,7 +119,7 @@ const EBRWExpressionIdeasLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
940
src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx
Normal file
940
src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx
Normal file
@ -0,0 +1,940 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
Check,
|
||||
BookOpen,
|
||||
AlertTriangle,
|
||||
Zap,
|
||||
Clock,
|
||||
Users,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
FORM_STRUCTURE_EASY,
|
||||
FORM_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/form-structure-sense";
|
||||
import DecisionTreeWidget, {
|
||||
type TreeScenario,
|
||||
type TreeNode,
|
||||
} from "../../../components/lessons/DecisionTreeWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const SVA_DISGUISES: RevealCard[] = [
|
||||
{
|
||||
label: "Prepositional Phrase Decoy",
|
||||
sublabel: "The list [of requirements] __ long.",
|
||||
content: 'Cross out the phrase. "List" is singular → IS long.',
|
||||
},
|
||||
{
|
||||
label: "Inverted Order (There is/are)",
|
||||
sublabel: "There __ many reasons for the decision.",
|
||||
content:
|
||||
'"Many reasons" is the subject (plural) → THERE ARE. Flip: "Many reasons there are."',
|
||||
},
|
||||
{
|
||||
label: "Gerund Subject",
|
||||
sublabel: "Studying the data __ essential.",
|
||||
content:
|
||||
"A gerund (-ing phrase) as subject is always singular → IS essential.",
|
||||
},
|
||||
{
|
||||
label: "Collective Nouns",
|
||||
sublabel: "The committee __ voted unanimously.",
|
||||
content:
|
||||
"Committee = one group = singular → HAS voted. Same for team, group, family, audience, class.",
|
||||
},
|
||||
{
|
||||
label: "Indefinite Pronouns",
|
||||
sublabel: "Each of the participants __ required.",
|
||||
content: "Each is always singular → IS required.",
|
||||
},
|
||||
{
|
||||
label: "Neither/Nor Proximity",
|
||||
sublabel: "Neither the principal nor the teachers __",
|
||||
content:
|
||||
"Or/nor: agree with the NEAREST noun → teachers (plural) → ARE responsible.",
|
||||
},
|
||||
];
|
||||
|
||||
const PRONOUN_RULES: RevealCard[] = [
|
||||
{
|
||||
label: "Number Agreement",
|
||||
content:
|
||||
'Pronoun must match its antecedent in number. "Each student should bring their own materials" is accepted on SAT for singular "they."',
|
||||
},
|
||||
{
|
||||
label: "This/That Must Have a Clear Referent",
|
||||
content:
|
||||
'Always make sure "this" or "that" refers to a specific noun, not a vague idea. If unclear, replace with the noun.',
|
||||
},
|
||||
{
|
||||
label: "Emphatic Pronouns",
|
||||
content:
|
||||
'These (himself/herself/themselves) can NEVER be the subject. ✗ "Himself was the only one who knew." ✓ "He himself was the only one who knew."',
|
||||
},
|
||||
{
|
||||
label: "Missing Antecedent",
|
||||
content:
|
||||
'Every pronoun must refer to a specific named noun. If "they" could refer to two different nouns, replace it with the noun it means.',
|
||||
},
|
||||
{
|
||||
label: "Person Consistency",
|
||||
content:
|
||||
'Do not switch between "one," "you," and "we" within the same passage. Match the pronoun person already established.',
|
||||
},
|
||||
];
|
||||
|
||||
const SVA_TREE: TreeNode = {
|
||||
id: "sva-root",
|
||||
question:
|
||||
"Is there a prepositional phrase or interrupting clause BETWEEN the subject and verb?",
|
||||
hint: 'Cross out everything introduced by "of," "in," "with," "for," "by," "including," etc., up to the next punctuation.',
|
||||
yesLabel: "Yes — there is interrupting material",
|
||||
noLabel: "No — subject and verb are adjacent",
|
||||
yes: {
|
||||
id: "cross-out",
|
||||
question:
|
||||
"After crossing out the interrupting material, is the true subject singular or plural?",
|
||||
hint: "The true subject is the noun BEFORE the interrupting phrase. Ignore the noun inside the phrase.",
|
||||
yesLabel: "Singular subject",
|
||||
noLabel: "Plural subject",
|
||||
yes: {
|
||||
id: "singular-verb",
|
||||
question: "",
|
||||
result:
|
||||
"✓ Use a SINGULAR verb (is, was, has, does). The interrupting phrase cannot change the subject.",
|
||||
resultType: "correct",
|
||||
ruleRef: 'The list [of requirements] IS long — not "are"',
|
||||
},
|
||||
no: {
|
||||
id: "plural-verb",
|
||||
question: "",
|
||||
result:
|
||||
"✓ Use a PLURAL verb (are, were, have, do). The interrupting phrase cannot change the subject.",
|
||||
resultType: "correct",
|
||||
ruleRef: 'The scientists [in the lab] ARE conducting — not "is"',
|
||||
},
|
||||
},
|
||||
no: {
|
||||
id: "no-interrupt",
|
||||
question:
|
||||
"Is the subject an indefinite pronoun (each, every, either, neither, anyone, everyone, someone, nobody)?",
|
||||
hint: "These pronouns are always grammatically singular even when they refer to groups.",
|
||||
yesLabel: "Yes — indefinite pronoun subject",
|
||||
noLabel: "No — regular noun or pronoun",
|
||||
yes: {
|
||||
id: "indefinite",
|
||||
question: "",
|
||||
result:
|
||||
"✓ Use a SINGULAR verb. Each, every, either, neither, anyone, everyone, someone, nobody are all grammatically singular.",
|
||||
resultType: "correct",
|
||||
ruleRef: 'Each of the students IS responsible — not "are"',
|
||||
},
|
||||
no: {
|
||||
id: "regular-agree",
|
||||
question: "",
|
||||
result:
|
||||
"✓ Match the verb to the subject normally: singular noun = singular verb; plural noun = plural verb.",
|
||||
resultType: "info",
|
||||
ruleRef: "Standard agreement: The dog barks / The dogs bark",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const TREE_SCENARIOS: TreeScenario[] = [
|
||||
{
|
||||
label: "SVA 1",
|
||||
sentence:
|
||||
"The collection of ancient manuscripts were donated to the university library.",
|
||||
tree: SVA_TREE,
|
||||
},
|
||||
{
|
||||
label: "SVA 2",
|
||||
sentence:
|
||||
"Neither of the proposed solutions address the root cause of the problem.",
|
||||
tree: SVA_TREE,
|
||||
},
|
||||
{
|
||||
label: "SVA 3",
|
||||
sentence:
|
||||
"The team of researchers have published their findings in three separate journals.",
|
||||
tree: SVA_TREE,
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWFormStructureSenseLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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="Subject-Verb Agreement"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Verb Forms & Tense"
|
||||
icon={Clock}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={2}
|
||||
title="Pronouns & Apostrophes"
|
||||
icon={Users}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={3}
|
||||
title="Modifiers & Parallel"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={4} title="SVA Decision Tree" icon={GitBranch} />
|
||||
<SectionMarker index={5} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* ── Section 0: Subject-Verb Agreement ── */}
|
||||
<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 — Domain 3
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Form, Structure & Sense
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Seven grammar skills: subject-verb agreement, verb forms, pronouns,
|
||||
apostrophes, modification, parallel structure, and comma usage.
|
||||
</p>
|
||||
|
||||
{/* §8.1 SVA */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-purple-50 border border-purple-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-purple-900">
|
||||
Subject-Verb Agreement: All 6 Disguises
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The SAT hides the subject behind phrases and structural tricks.
|
||||
Tap each disguise to see how to solve it:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={SVA_DISGUISES}
|
||||
columns={3}
|
||||
accentColor="purple"
|
||||
/>
|
||||
<div className="bg-white border border-purple-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Indefinite Pronoun Quick Reference
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-purple-100">
|
||||
<th className="px-3 py-1 text-left text-purple-800">
|
||||
Always Singular
|
||||
</th>
|
||||
<th className="px-3 py-1 text-left text-purple-800">
|
||||
Always Plural
|
||||
</th>
|
||||
<th className="px-3 py-1 text-left text-purple-800">
|
||||
Context-Dependent
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="bg-white">
|
||||
<td className="px-3 py-2 text-slate-600">
|
||||
each, every, either, neither, one, anyone, everyone,
|
||||
someone, no one, nobody, somebody, everybody, anything,
|
||||
everything, something, nothing
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600">
|
||||
both, few, many, several, others
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600">
|
||||
all, any, more, most, none, some — match to the noun in
|
||||
the "of" phrase: "most of the water IS" / "most of the
|
||||
results ARE"
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 1: Verb Forms & Tense ── */}
|
||||
<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">
|
||||
Verb Forms & Tense
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
The SAT tests specific verb form errors, not just tense consistency.
|
||||
</p>
|
||||
|
||||
{/* Tense Table */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-purple-50 border border-purple-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-purple-900">
|
||||
Tense Consistency
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-purple-600 text-white">
|
||||
<th className="px-3 py-2 text-left text-xs">Tense</th>
|
||||
<th className="px-3 py-2 text-left text-xs">
|
||||
Signal Words
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
[
|
||||
"Simple Present",
|
||||
"now, every day, always, usually, generally",
|
||||
"The study examines 500 participants.",
|
||||
],
|
||||
[
|
||||
"Simple Past",
|
||||
"yesterday, last year, in 1990, ago",
|
||||
"The study examined 500 participants.",
|
||||
],
|
||||
[
|
||||
"Present Perfect",
|
||||
"since, for, recently, already, yet",
|
||||
"Researchers have found new evidence.",
|
||||
],
|
||||
[
|
||||
"Past Perfect",
|
||||
"before, by the time, had already",
|
||||
"By 2020, scientists had confirmed the theory.",
|
||||
],
|
||||
[
|
||||
"Future",
|
||||
"tomorrow, next year, will",
|
||||
"The team will publish next month.",
|
||||
],
|
||||
[
|
||||
"Progressive",
|
||||
"currently, at the moment, was/is + -ing",
|
||||
"She was collecting data when the power failed.",
|
||||
],
|
||||
].map(([tense, signals, ex], i) => (
|
||||
<tr
|
||||
key={tense}
|
||||
className={i % 2 === 0 ? "bg-white" : "bg-purple-50"}
|
||||
>
|
||||
<td className="px-3 py-2 font-bold text-purple-800 text-xs">
|
||||
{tense}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-500 text-xs">
|
||||
{signals}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-700 text-xs italic">
|
||||
{ex}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Past Conditional */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Past Conditional & Irregular Past Participles
|
||||
</h3>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Past Conditional (Counterfactual)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
Use <span className="font-bold">had + past participle</span> in
|
||||
the "if" clause and{" "}
|
||||
<span className="font-bold">would have + past participle</span>{" "}
|
||||
in the result clause.
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li className="text-red-600">
|
||||
✗ "If she would have studied harder, she would have passed."
|
||||
</li>
|
||||
<li className="text-green-700">
|
||||
✓ "If she <strong>had studied</strong> harder, she{" "}
|
||||
<strong>would have passed</strong>."
|
||||
</li>
|
||||
<li className="text-slate-400 italic mt-1">
|
||||
The SAT frequently places "would have" in the if-clause —
|
||||
always wrong.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-slate-700 text-white">
|
||||
<th className="px-3 py-2 text-left">Base Form</th>
|
||||
<th className="px-3 py-2 text-left">Simple Past</th>
|
||||
<th className="px-3 py-2 text-left">
|
||||
Past Participle (with have/had)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["go", "went", "gone (had gone ✓ / had went ✗)"],
|
||||
["rise", "rose", "risen (had risen ✓ / had rose ✗)"],
|
||||
["lie (recline)", "lay", "lain (had lain ✓)"],
|
||||
["lay (place)", "laid", "laid (had laid ✓)"],
|
||||
["choose", "chose", "chosen (had chosen ✓)"],
|
||||
["swim", "swam", "swum (had swum ✓ / had swam ✗)"],
|
||||
["begin", "began", "begun (had begun ✓ / had began ✗)"],
|
||||
["see", "saw", "seen (had seen ✓ / had saw ✗)"],
|
||||
].map(([base, past, participle], i) => (
|
||||
<tr
|
||||
key={base}
|
||||
className={i % 2 === 0 ? "bg-white" : "bg-slate-50"}
|
||||
>
|
||||
<td className="px-3 py-2 font-bold text-purple-800">
|
||||
{base}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600">{past}</td>
|
||||
<td className="px-3 py-2 text-slate-700">{participle}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-2">
|
||||
To vs. -ing After Certain Verbs
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
||||
{[
|
||||
{
|
||||
label: "Verbs that take TO",
|
||||
words:
|
||||
"want, hope, plan, decide, need, agree, refuse, offer, expect, seem",
|
||||
},
|
||||
{
|
||||
label: "Verbs that take -ING",
|
||||
words:
|
||||
"enjoy, avoid, finish, consider, deny, practice, suggest, admit, keep, quit",
|
||||
},
|
||||
{
|
||||
label: "Meaning changes",
|
||||
words:
|
||||
"stop to do (pause) / stop doing (cease); remember to do / remember doing",
|
||||
},
|
||||
].map((v) => (
|
||||
<div key={v.label}>
|
||||
<p className="font-bold text-amber-700 mb-1">{v.label}</p>
|
||||
<p className="text-slate-600">{v.words}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Passive Voice
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-3">
|
||||
Passive voice reverses the usual subject-verb-object order: the
|
||||
receiver of the action becomes the grammatical subject. The SAT
|
||||
does not treat passive voice as inherently wrong — but it tests
|
||||
whether you can identify the correct form.
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs mb-3">
|
||||
<li className="text-slate-600">
|
||||
<span className="font-bold">Active:</span> "The researcher
|
||||
collected the data."
|
||||
</li>
|
||||
<li className="text-slate-600">
|
||||
<span className="font-bold">Passive:</span> "The data was
|
||||
collected by the researcher."
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xs text-slate-500 italic">
|
||||
Passive is formed with a form of 'be' + past participle. On the
|
||||
SAT, check whether the passive form matches the correct tense
|
||||
and whether the subject-verb agreement is correct in the passive
|
||||
construction.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 2: Pronouns & Apostrophes ── */}
|
||||
<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-2">
|
||||
Pronouns & Apostrophes
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Pronoun errors are among the most commonly tested items in Form,
|
||||
Structure & Sense.
|
||||
</p>
|
||||
|
||||
{/* Relative Pronouns */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-purple-50 border border-purple-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-purple-900">Pronoun Rules</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-purple-600 text-white">
|
||||
<th className="px-3 py-2 text-left text-xs">Pronoun</th>
|
||||
<th className="px-3 py-2 text-left text-xs">Used For</th>
|
||||
<th className="px-3 py-2 text-left text-xs">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
[
|
||||
"who / whom",
|
||||
"People only",
|
||||
'"the researcher who discovered…" / "the scientist whom they selected…"',
|
||||
],
|
||||
[
|
||||
"which",
|
||||
"Things / non-people (non-essential)",
|
||||
'"the study, which examined 500 people…" — always gets a comma',
|
||||
],
|
||||
[
|
||||
"that",
|
||||
"Things / non-people (essential)",
|
||||
'"the study that confirmed the theory" — no comma',
|
||||
],
|
||||
[
|
||||
"where",
|
||||
"Places",
|
||||
'"the laboratory where the experiment occurred…"',
|
||||
],
|
||||
["when", "Times", '"the decade when the research began…"'],
|
||||
].map(([pron, use, ex], i) => (
|
||||
<tr
|
||||
key={pron}
|
||||
className={i % 2 === 0 ? "bg-white" : "bg-purple-50"}
|
||||
>
|
||||
<td className="px-3 py-2 font-bold text-purple-800 text-xs whitespace-nowrap">
|
||||
{pron}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600 text-xs">
|
||||
{use}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600 text-xs italic">
|
||||
{ex}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="bg-purple-900 text-white rounded-xl p-4">
|
||||
<p className="font-bold text-sm mb-1">
|
||||
Who vs. Whom — Substitution Test
|
||||
</p>
|
||||
<ul className="text-xs text-purple-100 space-y-1">
|
||||
<li>
|
||||
<span className="font-bold">Who</span> = subject (substitutes
|
||||
he/she): "Who wrote the report?" → "He wrote it." ✓
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Whom</span> = object (substitutes
|
||||
him/her): "To whom did you give it?" → "You gave it to him." ✓
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-slate-800">
|
||||
Additional Pronoun Rules — tap to reveal:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={PRONOUN_RULES}
|
||||
columns={3}
|
||||
accentColor="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Apostrophes */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">Apostrophes</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Noun Apostrophes (Possession)
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>
|
||||
<span className="font-bold">Singular noun:</span> add 's →
|
||||
the scientist's data
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Plural noun ending in s:</span>{" "}
|
||||
add ' → the scientists' lab
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
Plural noun NOT ending in s:
|
||||
</span>{" "}
|
||||
add 's → the children's results
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Pronoun Apostrophes (Contractions Only)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-xs">
|
||||
{[
|
||||
["its", "possessive"],
|
||||
["it's", "it is / it has"],
|
||||
["their", "possessive"],
|
||||
["they're", "they are"],
|
||||
["whose", "possessive"],
|
||||
["who's", "who is / who has"],
|
||||
["your", "possessive"],
|
||||
["you're", "you are"],
|
||||
].map(([form, meaning]) => (
|
||||
<div key={form} className="flex gap-1">
|
||||
<span className="font-bold text-purple-700">{form}</span>
|
||||
<span className="text-slate-500">= {meaning}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
Apostrophe Shortcut
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
If you can read the word as two words (it is → it's), it's a
|
||||
contraction — use the apostrophe. Pronouns{" "}
|
||||
<span className="font-bold">never</span> use apostrophes for
|
||||
possession (opposite of nouns).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 3: Modifiers, Parallel Structure & Commas ── */}
|
||||
<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-2">
|
||||
Modifiers, Parallel Structure & Commas
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Three interrelated skills that test whether elements are correctly
|
||||
placed and balanced.
|
||||
</p>
|
||||
|
||||
{/* §8.5 Modification */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-purple-50 border border-purple-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-purple-900">
|
||||
Modification Errors
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="card-tilt bg-white border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-2">
|
||||
Dangling Modifier
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mb-2">
|
||||
The modifier has no logical subject in the sentence, or the
|
||||
subject it should modify does not immediately follow it.
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li className="text-red-600">
|
||||
✗ "Running quickly, the bus was missed." — The bus isn't
|
||||
running.
|
||||
</li>
|
||||
<li className="text-green-700">
|
||||
✓ "Running quickly, she missed the bus." — She was running;
|
||||
subject appears right after comma.
|
||||
</li>
|
||||
<li className="text-amber-700 italic mt-1">
|
||||
Possessive Trap: "Wishing to pass,{" "}
|
||||
<strong>the student's</strong> exam…" — the possessive makes
|
||||
"exam" the subject. Use: "Wishing to pass,{" "}
|
||||
<strong>the student</strong> reviewed…"
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-2">
|
||||
Misplaced Modifier
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mb-2">
|
||||
The modifier is positioned too far from the noun it modifies.
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li className="text-red-600">
|
||||
✗ "She only eats salad." — only implies she does nothing
|
||||
else?
|
||||
</li>
|
||||
<li className="text-green-700">
|
||||
✓ "She eats only salad." — only modifies "salad."
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-purple-800 text-sm mb-2">
|
||||
Parenthetical Modifiers — Matching Punctuation
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mb-2">
|
||||
A non-essential phrase must be enclosed in matching
|
||||
punctuation: two commas, two dashes, or two parentheses. Never
|
||||
mix them.
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li className="text-red-600">
|
||||
✗ "The researcher, who had published 40 papers— presented
|
||||
her findings."
|
||||
</li>
|
||||
<li className="text-green-700">
|
||||
✓ "The researcher, who had published 40 papers, presented
|
||||
her findings."
|
||||
</li>
|
||||
<li className="text-green-700">
|
||||
✓ "The researcher — who had published 40 papers — presented
|
||||
her findings."
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* §8.6 Parallel Structure */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Parallel Structure
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{
|
||||
type: "Lists",
|
||||
bad: "She enjoys hiking, to swim, and reading.",
|
||||
good: "She enjoys hiking, swimming, and reading.",
|
||||
note: "All items must be the same grammatical form.",
|
||||
},
|
||||
{
|
||||
type: "Correlative Pairs (not only…but also, both…and, either…or)",
|
||||
bad: "He not only speaks French but also is fluent in German.",
|
||||
good: "He not only speaks French but also speaks German.",
|
||||
note: 'What follows "not only" must be parallel to what follows "but also."',
|
||||
},
|
||||
{
|
||||
type: "Comparisons",
|
||||
bad: "Running is healthier than to sit all day.",
|
||||
good: "Running is healthier than sitting all day.",
|
||||
note: "Both sides of a comparison must be the same form.",
|
||||
},
|
||||
{
|
||||
type: "Semicolon Lists",
|
||||
bad: "The study examined diet; the exercise habits; and their stress levels.",
|
||||
good: "The study examined diet; exercise habits; and stress levels.",
|
||||
note: "Items separated by semicolons must be parallel.",
|
||||
},
|
||||
].map((p) => (
|
||||
<div
|
||||
key={p.type}
|
||||
className="card-tilt bg-slate-50 border border-slate-200 rounded-xl p-4"
|
||||
>
|
||||
<p className="font-bold text-purple-800 text-xs mb-1">
|
||||
{p.type}
|
||||
</p>
|
||||
<p className="text-xs text-red-600">✗ {p.bad}</p>
|
||||
<p className="text-xs text-green-700">✓ {p.good}</p>
|
||||
<p className="text-xs text-slate-400 italic mt-1">{p.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* §8.7 Commas */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-purple-50 border border-purple-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-purple-900">Comma Rules</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-green-200 rounded-xl p-4">
|
||||
<p className="font-bold text-green-800 text-sm mb-2">
|
||||
When a Comma IS Required
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>
|
||||
✓ After an introductory element at the start of a sentence.
|
||||
</li>
|
||||
<li>
|
||||
✓ Before and after a non-essential (parenthetical) clause.
|
||||
</li>
|
||||
<li>✓ Before a FANBOYS joining two independent clauses.</li>
|
||||
<li>✓ Between items in a list of three or more.</li>
|
||||
<li>
|
||||
✓ Between coordinate adjectives that independently modify
|
||||
the noun.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-2">
|
||||
When a Comma Is NOT Used
|
||||
</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>✗ Between a subject and its verb.</li>
|
||||
<li>
|
||||
✗ Before a subordinate clause that follows the main clause
|
||||
("left because she was tired" — no comma before "because").
|
||||
</li>
|
||||
<li>
|
||||
✗ Before a FANBOYS when one side is a phrase, not an IC.
|
||||
</li>
|
||||
<li>✗ After a FANBOYS conjunction.</li>
|
||||
<li>✗ Between a verb and its object or complement.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-purple-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-purple-100">
|
||||
For SVA: always cross out the material between subject and verb.
|
||||
For modifiers: the noun being modified must immediately follow the
|
||||
modifying phrase. For apostrophes: pronouns never use apostrophes
|
||||
for possession — only for contractions.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 4: SVA Decision Tree ── */}
|
||||
<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-2">
|
||||
SVA Decision Tree
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Work through subject-verb agreement step by step. Find the true
|
||||
subject, then match the verb.
|
||||
</p>
|
||||
<DecisionTreeWidget scenarios={TREE_SCENARIOS} accentColor="purple" />
|
||||
</section>
|
||||
|
||||
{/* ── Section 5: Practice ── */}
|
||||
<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">
|
||||
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 EBRWFormStructureSenseLesson;
|
||||
@ -287,7 +287,7 @@ const EBRWGraphicDisplaysLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
@ -1,60 +1,107 @@
|
||||
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 { Check, BookOpen, Lightbulb, Zap, Target } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
INFERENCES_EASY,
|
||||
INFERENCES_MEDIUM,
|
||||
} from "../../../data/rw/inferences";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const INFERENCE_TYPES: RevealCard[] = [
|
||||
{
|
||||
label: "Valid / Necessary",
|
||||
sublabel: "CORRECT on SAT",
|
||||
content:
|
||||
"Must be true if the stated evidence is true. Example: If 14% of earthquakes are supershear → 86% are NOT supershear.",
|
||||
},
|
||||
{
|
||||
label: "Possible / Speculative",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
'Could be true, but might not be; goes further than the evidence. Example: "Researchers must want more funding" — not stated.',
|
||||
},
|
||||
{
|
||||
label: "Contradicted",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
'Directly conflicts with stated evidence. Example: "Exercise improves fitness equally for all" when passage says otherwise.',
|
||||
},
|
||||
{
|
||||
label: "Real-world true but unsupported",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
"True in reality but not implied by the passage. The SAT only rewards what the text guarantees.",
|
||||
},
|
||||
{
|
||||
label: "Half-valid",
|
||||
sublabel: "WRONG on SAT",
|
||||
content:
|
||||
"First part follows from the evidence, but the second part goes beyond what the evidence requires.",
|
||||
},
|
||||
];
|
||||
|
||||
const CHAIN_ERRORS: RevealCard[] = [
|
||||
{
|
||||
label: "Stopping too early",
|
||||
content:
|
||||
'Stopping at the finding rather than the conclusion (e.g., stopping at "feldspar was found" rather than following its implication for the competing theories).',
|
||||
},
|
||||
{
|
||||
label: "Skipping a step",
|
||||
content:
|
||||
"Jumping to a conclusion that goes further than the chain allows by skipping an intermediate logical step.",
|
||||
},
|
||||
{
|
||||
label: "Scope confusion",
|
||||
content:
|
||||
"The chain applies to one specific region or group, but the answer makes a universal claim about all regions or groups.",
|
||||
},
|
||||
{
|
||||
label: "Time assumption",
|
||||
content:
|
||||
"The evidence describes a current or past state, and the answer makes a claim about a future state without justification.",
|
||||
},
|
||||
];
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"What can be inferred from this passage about the long-term effects of the policy?",
|
||||
"Based on the passage, which sentence provides the strongest basis for inferring that Dr. Patel's research contradicts established scientific consensus?",
|
||||
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.",
|
||||
"Dr. Patel has spent fifteen years studying migration patterns of monarch butterflies.",
|
||||
"Her field stations span the entire North American flyway, from Canada to Mexico.",
|
||||
"She has published 47 peer-reviewed papers, each building on data collected across multiple seasons.",
|
||||
"Her most recent paper challenges the assumption that butterflies navigate primarily by magnetic fields.",
|
||||
"Instead, she proposes that polarized light plays a more significant role than previously recognized.",
|
||||
],
|
||||
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.",
|
||||
'Sentence 4 is the basis for inferring contradiction with consensus. The word "challenges" indicates Dr. Patel is actively contradicting an established "assumption." This makes the inference valid — not speculation — because the passage directly states she is challenging the field.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"What does the passage imply about the relationship between diet and cognitive decline?",
|
||||
"Which sentence provides the strongest basis for inferring that the plastic bag ban had unintended consequences?",
|
||||
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.",
|
||||
"In 2015, the city council banned plastic bags at all grocery stores.",
|
||||
"The ban was intended to reduce plastic waste in local waterways.",
|
||||
"Plastic bag litter in rivers decreased by 40% in the first year.",
|
||||
"However, sales of heavier-gauge trash bags increased by 350% over the same period.",
|
||||
"Environmental analysts noted that thick trash bags contain more plastic by weight than the thin bags they replaced.",
|
||||
],
|
||||
evidenceIndex: 2,
|
||||
evidenceIndex: 3,
|
||||
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.",
|
||||
"Sentence 4 is the evidence of the unintended consequence — a massive surge in heavier plastic bag purchases. Combined with sentence 5, it implies total plastic use may have increased, the opposite of the ban's stated goal.",
|
||||
},
|
||||
];
|
||||
|
||||
@ -78,9 +125,11 @@ const EBRWInferencesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
return () => observers.forEach((o) => o.disconnect());
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (i: number) => {
|
||||
setActiveSection(i);
|
||||
sectionsRef.current[i]?.scrollIntoView({ behavior: "smooth" });
|
||||
useScrollReveal();
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
setActiveSection(index);
|
||||
sectionsRef.current[index]?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const SectionMarker = ({
|
||||
@ -100,7 +149,8 @@ const EBRWInferencesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
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"}`}
|
||||
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" />
|
||||
@ -119,192 +169,463 @@ const EBRWInferencesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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={0} title="Valid Inferences" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Evidence Hunter"
|
||||
icon={AlertTriangle}
|
||||
title="Inference Patterns"
|
||||
icon={Lightbulb}
|
||||
/>
|
||||
<SectionMarker index={2} title="Practice Quiz" icon={Zap} />
|
||||
|
||||
<SectionMarker index={2} title="Inference Tracker" icon={Target} />
|
||||
<SectionMarker index={3} 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 & Annotation */}
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* Section 0 — Valid Inferences */}
|
||||
<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 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 w-fit">
|
||||
Information & Ideas — Domain 2
|
||||
</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.
|
||||
A valid inference MUST be true based on the passage — not merely
|
||||
possible, plausible, or consistent.
|
||||
</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}
|
||||
{/* Core Concept — now with RevealCardGrid */}
|
||||
<div className="scroll-reveal stagger-1 bg-teal-50 border border-teal-200 rounded-2xl p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-teal-900 mb-2">
|
||||
The Core Concept — What Makes an Inference Valid?
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700 mb-4">
|
||||
A valid inference is a statement that MUST be true if the evidence
|
||||
is true. It cannot merely be likely, plausible, or consistent. The
|
||||
SAT rewards only conclusions that necessarily follow from what is
|
||||
stated.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={INFERENCE_TYPES}
|
||||
columns={3}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
{rule.body}
|
||||
|
||||
{/* 3-Step Text Completion Process */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
The 3-Step Text Completion Process
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Text completion questions present a short passage followed by a
|
||||
blank at the end, asking "Which choice most logically completes
|
||||
the text?" The correct answer NECESSARILY follows from the stated
|
||||
evidence — not merely the most interesting or plausible
|
||||
continuation.
|
||||
</p>
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
Universal Method for All Inference Questions
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
[
|
||||
"1",
|
||||
"IDENTIFY the key claim or evidence (usually in the 1–2 sentences before the blank). Write it in 3–6 words on scratch paper.",
|
||||
],
|
||||
[
|
||||
"2",
|
||||
'WORK OUT the implication: "If this is true, what MUST also be true?" Do this BEFORE looking at answers. Even one word helps.',
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"MATCH the answer: look for the option that says the same thing as your implication, possibly using different words, negation, or synonyms.",
|
||||
],
|
||||
].map(([n, text]) => (
|
||||
<div key={n} className="flex gap-2">
|
||||
<span className="w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{n}
|
||||
</span>
|
||||
<p className="text-xs text-slate-700">{text}</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
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">
|
||||
WHY THIS MATTERS
|
||||
</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 className="text-xs text-slate-700">
|
||||
If you look at answer choices before working out the
|
||||
implication, you are vulnerable to speculation traps — answers
|
||||
that sound plausible because they extend the idea in a
|
||||
reasonable direction, but go further than the evidence requires.
|
||||
</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 1 — Inference Patterns */}
|
||||
<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
|
||||
Inference Patterns
|
||||
</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?
|
||||
The four core patterns, speculation traps, double negatives, and
|
||||
multi-step chains.
|
||||
</p>
|
||||
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
{/* 6A — 4 Core Patterns */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
The Four Core Valid Inference Patterns
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
pattern: "Negation / Contrapositive",
|
||||
rule: "If only X has property Y, then everything that is not-X must lack property Y. This works in both directions.",
|
||||
examples: [
|
||||
"If 14% of earthquakes are supershear events → 86% are NOT supershear events.",
|
||||
"If a phenomenon occurs ONLY during slow-wave sleep → it does NOT occur during REM sleep.",
|
||||
"If researchers focused mostly on land → most data comes from land, therefore they may have undercounted non-land events.",
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
pattern: "Relative Comparison",
|
||||
rule: "If X is more than Y, you can restate this as Y is less than X. If X is the most, you can infer that all others are less than X.",
|
||||
examples: [
|
||||
"If muscular contractions when lowering weights are MOST effective → contractions when raising weights are LESS effective.",
|
||||
"If big brown bats emit the most cries → all other species emit fewer cries.",
|
||||
"Restatement direction: if the claim is A > B, a valid answer might say B < A.",
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
pattern: "Logical Elimination",
|
||||
rule: "When possibilities are listed and most are ruled out, the remaining possibility is the valid inference.",
|
||||
examples: [
|
||||
"Researchers studied supershear earthquakes mostly on land → they did not study underwater earthquakes → many supershear earthquakes likely occur underwater.",
|
||||
"Two theories for Mars's crust: (a) magma ocean or (b) different origin. If feldspar (associated with b) is found → theory (a) alone does not explain the crust.",
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
pattern: "Causal / Consequential Extension",
|
||||
rule: "If X causes Y, then applying X to a new situation should produce Y. Removing X should reduce Y.",
|
||||
examples: [
|
||||
"If replay during slow-wave sleep consolidates memory → dancers who sleep several hours right after learning a routine should remember it better two weeks later.",
|
||||
"If hyperglycemia (high blood sugar) reduces exercise response → a drug that lowers blood sugar should improve exercise response.",
|
||||
"Apply the mechanism to a new group, new time period, or new scenario — but STAY within the same causal framework.",
|
||||
],
|
||||
},
|
||||
].map((p) => (
|
||||
<div
|
||||
key={p.num}
|
||||
className="card-tilt bg-white border border-slate-200 rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="w-6 h-6 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{p.num}
|
||||
</span>
|
||||
<p className="font-bold text-teal-900">{p.pattern}</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mb-2">{p.rule}</p>
|
||||
<ul className="space-y-1">
|
||||
{p.examples.map((ex, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="text-xs text-slate-500 bg-slate-50 rounded px-2 py-1 italic"
|
||||
>
|
||||
• {ex}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6B — Speculation Traps */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Speculation Traps
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Speculation traps are the most common wrong answer type in text
|
||||
completions. They go one plausible step too far beyond what the
|
||||
evidence necessarily implies.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-2">
|
||||
How to Identify a Speculation Trap
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• The answer could be true but doesn't have to be.</li>
|
||||
<li>
|
||||
• The answer introduces a new assumption not grounded in the
|
||||
passage.
|
||||
</li>
|
||||
<li>
|
||||
• The answer requires you to imagine a scenario the passage
|
||||
doesn't describe.
|
||||
</li>
|
||||
<li>
|
||||
• The answer goes from "this happens" to "this is the
|
||||
best/only/most effective way."
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
Worked Example
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2 italic">
|
||||
Evidence: Sea turtle conservation focuses on protecting
|
||||
hatchlings after they emerge.
|
||||
</p>
|
||||
<ul className="text-xs space-y-1">
|
||||
<li className="text-green-700">
|
||||
✓ VALID: Conservation focuses less on hatchlings before they
|
||||
emerge.
|
||||
</li>
|
||||
<li className="text-red-600">
|
||||
✗ TRAP: Protecting hatchlings after emergence is the only
|
||||
effective method.
|
||||
</li>
|
||||
<li className="text-red-600">
|
||||
✗ TRAP: Pre-emergence protection is more effective.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-800 text-sm mb-1">RULE</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
The word "only" in an answer choice is almost always a sign of
|
||||
over-speculation. Very few things on the SAT are literally "the
|
||||
only way." Be very suspicious of absolute claims in answer
|
||||
choices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6C — Double Negatives & Second Meanings */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Double Negatives and Second Meanings in Answer Choices
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Inference answer choices frequently use double negatives (which
|
||||
create positive meanings) or words in their second meanings. These
|
||||
are designed to make correct answers look wrong to careless
|
||||
readers.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
Double Negative Translations
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• "Not impossible" = possible</li>
|
||||
<li>• "Not unimportant" = important</li>
|
||||
<li>• "Not unlike" = similar</li>
|
||||
<li>• "Less harmful" = milder, but still harmful</li>
|
||||
<li>• Decode completely before evaluating the answer.</li>
|
||||
<li className="italic text-teal-700">
|
||||
Strategy: Replace "not un-X" with "X" and "not im-X" with
|
||||
"X".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
Second Meanings to Know
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• "Qualify" = limit the scope of a claim (not: meet
|
||||
requirements)
|
||||
</li>
|
||||
<li>• "Sound" = valid, reliable (not: audio)</li>
|
||||
<li>• "Check" = restrain, control (not: verify)</li>
|
||||
<li>
|
||||
• "Economy" = thrift, efficiency (not: the financial system)
|
||||
</li>
|
||||
<li>• "Reserve" = hold off on (not: book in advance)</li>
|
||||
<li className="italic text-teal-700">
|
||||
When a simple common word appears as an answer, suspect a
|
||||
second meaning.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6D — Multi-Step Logical Chains */}
|
||||
<div className="scroll-reveal stagger-4 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Multi-Step Logical Chains
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Some text completion questions require you to follow 2–3 logical
|
||||
steps before arriving at the conclusion. Students most often lose
|
||||
points here by stopping one step too early or introducing an extra
|
||||
assumption.
|
||||
</p>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-slate-800 text-sm mb-2">
|
||||
Multi-Step Chain: Worked Example (Mars Crust)
|
||||
</p>
|
||||
<div className="space-y-1 text-xs text-slate-700">
|
||||
<p>
|
||||
<span className="font-bold">Step 1:</span> Two theories exist
|
||||
for the first Martian crust: (a) all-encompassing magma ocean,
|
||||
(b) different origin with high silica.
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">Step 2:</span> Researchers find
|
||||
feldspar (associated with high-silica lava flows) in 9
|
||||
locations on Mars.
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">Step 3:</span> Feldspar is
|
||||
evidence for Theory (b) — different origin with high silica.
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">Step 4:</span> If Theory (b)
|
||||
accounts for some locations, Theory (a) (magma ocean) could
|
||||
not have been ALL-ENCOMPASSING.
|
||||
</p>
|
||||
<p className="text-green-700 font-bold mt-2">
|
||||
Valid Conclusion: The magma ocean was not all-encompassing.
|
||||
</p>
|
||||
<p className="text-red-600 italic">
|
||||
Speculation Trap: "Portions of Mars' surface were never
|
||||
covered by a crust." (no evidence for this)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-slate-800">
|
||||
Common errors in multi-step chains:
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={CHAIN_ERRORS}
|
||||
columns={2}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* 6E — Scratch Paper */}
|
||||
<div className="scroll-reveal stagger-5 rounded-2xl p-6 mb-8 bg-teal-50 border border-teal-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-teal-900">
|
||||
Using Scratch Paper for Inference Questions
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
For text completions, scratch paper is not optional — it is
|
||||
essential. Writing down even a brief summary of the key claim and
|
||||
your predicted answer protects you from being seduced by
|
||||
plausible-sounding wrong answers.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-teal-100 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
What to Write
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>• 3–6 word summary of the key claim.</li>
|
||||
<li>
|
||||
• Arrow indicating direction: "X → Y" or "less X = more Y".
|
||||
</li>
|
||||
<li>
|
||||
• Your predicted answer in 3–5 words before looking at
|
||||
choices.
|
||||
</li>
|
||||
<li>
|
||||
• For multi-step chains: number each step (1) → (2) → (3).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-teal-100 rounded-xl p-4">
|
||||
<p className="font-bold text-teal-800 text-sm mb-2">
|
||||
What This Prevents
|
||||
</p>
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
<li>
|
||||
• Choosing a speculative answer because it sounded good.
|
||||
</li>
|
||||
<li>
|
||||
• Forgetting the specific constraint after reading 4 answer
|
||||
choices.
|
||||
</li>
|
||||
<li>• Losing track of the logical chain halfway through.</li>
|
||||
<li>
|
||||
• Selecting half-valid answers that address only part of the
|
||||
claim.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-teal-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-teal-100">
|
||||
Test every answer with: "Does the passage GUARANTEE this is true?"
|
||||
If you can imagine a scenario where the passage is correct but
|
||||
this answer is still false, eliminate it. Only the answer that
|
||||
MUST be true is correct. "Only," "best," and "most effective" in
|
||||
answer choices = almost always over-speculation.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Practice Quiz */}
|
||||
{/* Section 2 — Inference Tracker widget */}
|
||||
<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-2">
|
||||
Inference Tracker
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Click the sentence that provides the strongest basis for the
|
||||
inference. Which sentence GUARANTEES the conclusion?
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="teal"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Section 3 — Practice */}
|
||||
<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">
|
||||
Practice Quiz
|
||||
Practice Questions
|
||||
</h2>
|
||||
{INFERENCES_EASY.slice(0, 2).map((q) => (
|
||||
<PracticeFromDataset key={q.id} question={q} color="teal" />
|
||||
@ -317,7 +638,7 @@ const EBRWInferencesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
onClick={onFinish}
|
||||
className="px-6 py-3 bg-teal-900 text-white font-bold rounded-full hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Finish Lesson
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -116,7 +116,7 @@ const EBRWMainIdeaLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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}
|
||||
|
||||
@ -238,7 +238,7 @@ const EBRWPronounsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
633
src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx
Normal file
633
src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx
Normal file
@ -0,0 +1,633 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
Check,
|
||||
BookOpen,
|
||||
Lightbulb,
|
||||
Zap,
|
||||
Target,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
RHETORICAL_EASY,
|
||||
RHETORICAL_MEDIUM,
|
||||
} from "../../../data/rw/rhetorical-synthesis";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import RevealCardGrid, {
|
||||
type RevealCard,
|
||||
} from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
/* ── Data for RevealCardGrid widgets ── */
|
||||
const RHETORICAL_GOALS: RevealCard[] = [
|
||||
{
|
||||
label: "Introduce a Topic",
|
||||
sublabel: '"introduce," "background," "context"',
|
||||
content: "Present a new subject with context. No strong argument yet.",
|
||||
},
|
||||
{
|
||||
label: "Make a Comparison",
|
||||
sublabel: '"compare," "contrast," "differ," "between"',
|
||||
content:
|
||||
"Explicitly contrast OR compare two things. Must mention BOTH and their relationship.",
|
||||
},
|
||||
{
|
||||
label: "Emphasize Similarity",
|
||||
sublabel: '"similarity," "common," "alike," "both"',
|
||||
content:
|
||||
'Highlight what two things have IN COMMON. Both must appear; "both" or "similarly" usually required.',
|
||||
},
|
||||
{
|
||||
label: "Illustrate with Example",
|
||||
sublabel: '"example," "illustrate," "such as"',
|
||||
content:
|
||||
"Use a specific case to demonstrate a general principle. Must contain a concrete example.",
|
||||
},
|
||||
{
|
||||
label: "State a Limitation",
|
||||
sublabel: '"limitation," "drawback," "only," "cannot"',
|
||||
content:
|
||||
"Acknowledge a restriction, exception, or weakness of a claim or finding.",
|
||||
},
|
||||
{
|
||||
label: "Present Cause-Effect",
|
||||
sublabel: '"cause," "result," "lead to," "effect"',
|
||||
content:
|
||||
"Show a causal chain: X leads to Y, or X results in Y. The relationship must be directional.",
|
||||
},
|
||||
{
|
||||
label: "Provide Supporting Evidence",
|
||||
sublabel: '"support," "evidence," "data," "findings"',
|
||||
content:
|
||||
"Give data, statistics, or findings that back up a claim already made.",
|
||||
},
|
||||
{
|
||||
label: "Acknowledge Opposing View",
|
||||
sublabel: '"opposing," "counterargument," "critics"',
|
||||
content:
|
||||
'Present a counterargument before making your own point. "Critics argue…" structure.',
|
||||
},
|
||||
];
|
||||
|
||||
const WRONG_PATTERNS: RevealCard[] = [
|
||||
{
|
||||
label: "Wrong Goal",
|
||||
sublabel: "Check this FIRST",
|
||||
content:
|
||||
"Achieves a different purpose from what the question asks. Most common wrong answer type — check before verifying facts.",
|
||||
},
|
||||
{
|
||||
label: "Accurate But Irrelevant",
|
||||
content:
|
||||
"Uses real facts from the notes, but from the wrong notes — not the ones that serve the stated goal.",
|
||||
},
|
||||
{
|
||||
label: "Distorted Fact",
|
||||
content:
|
||||
"Changes a number, reverses a direction, or misrepresents what a note says. Always verify facts word by word.",
|
||||
},
|
||||
{
|
||||
label: "Incompatible Notes Combined",
|
||||
content:
|
||||
"Joins two notes that cannot logically work together, producing individually accurate but collectively incoherent sentences.",
|
||||
},
|
||||
{
|
||||
label: "Wrong Scope",
|
||||
content:
|
||||
"Too broad (covers more than goal requires) or too narrow (picks a detail that misses the stated purpose).",
|
||||
},
|
||||
{
|
||||
label: "Outside Information",
|
||||
content:
|
||||
"Claims something not found in any provided notes — even if generally true in the real world. If you can't point to the note, eliminate it.",
|
||||
},
|
||||
];
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question:
|
||||
"A student wants to EMPHASIZE A SIMILARITY between two renewable energy sources. Which sentence best accomplishes this goal?",
|
||||
passage: [
|
||||
"Wind energy capacity in the US grew by 14% in 2022.",
|
||||
"Solar energy capacity in the US also grew by 14% in 2022.",
|
||||
"Wind turbines require more land per megawatt than solar panels.",
|
||||
"Both wind and solar energy produce no direct carbon emissions during operation.",
|
||||
"Government subsidies for renewable energy totaled $15 billion in 2022.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
'Sentence 4 directly emphasizes a shared characteristic of both sources. The word "both" signals similarity, and "no direct carbon emissions" is the specific trait they share — this is exactly what the goal requires.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"A student wants to ACKNOWLEDGE AN OPPOSING VIEW about urban farming before presenting their own argument. Which sentence best accomplishes this goal?",
|
||||
passage: [
|
||||
"Urban farming initiatives have expanded in over 200 American cities.",
|
||||
"Critics argue that urban farms cannot produce food at a scale sufficient to affect food insecurity.",
|
||||
"Urban farms do provide fresh produce in food deserts, regardless of total volume.",
|
||||
"Community gardens have been shown to strengthen neighborhood social ties.",
|
||||
"The average urban farm yields 5 times more produce per square foot than conventional farms.",
|
||||
],
|
||||
evidenceIndex: 1,
|
||||
explanation:
|
||||
'Sentence 2 explicitly presents a counterargument with "Critics argue..." — the standard structure for acknowledging an opposing view. It sets up the concession the student needs before making their own point.',
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWRhetoricalSynthesisLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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>
|
||||
);
|
||||
};
|
||||
|
||||
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="The Core Concept" icon={BookOpen} />
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Goals & Strategy"
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<SectionMarker index={2} title="Worked Example" icon={Target} />
|
||||
<SectionMarker index={3} title="Goal Matcher" icon={Lightbulb} />
|
||||
<SectionMarker index={4} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* ── Section 0: The Core Concept ── */}
|
||||
<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 w-fit">
|
||||
Expression of Ideas — Domain 4
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Rhetorical Synthesis
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Every answer is judged on two things: does it achieve the stated
|
||||
goal, and does every fact come from the notes? Failing either test
|
||||
means wrong — no exceptions.
|
||||
</p>
|
||||
|
||||
{/* §9.1 — What Makes an Answer Correct */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-rose-50 border border-rose-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
The Two-Criteria Rule
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
Unlike most SAT questions, Rhetorical Synthesis does not ask you
|
||||
to find the "best" answer in a general sense. It asks you to find
|
||||
the answer that satisfies exactly two requirements simultaneously.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="card-tilt bg-white border border-rose-200 rounded-xl p-4">
|
||||
<p className="font-bold text-rose-900 text-sm mb-2">
|
||||
Criterion 1 — GOAL
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
The answer must accomplish exactly the rhetorical purpose
|
||||
stated in the question — no more, no less. An answer that does
|
||||
something different from the stated goal is automatically
|
||||
wrong, even if every fact is accurate.
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-tilt bg-white border border-rose-200 rounded-xl p-4">
|
||||
<p className="font-bold text-rose-900 text-sm mb-2">
|
||||
Criterion 2 — ACCURACY
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
Every fact in the answer must come directly from the provided
|
||||
notes — nothing added, nothing distorted. Even one wrong
|
||||
number or reversed relationship eliminates an answer, even if
|
||||
the goal is otherwise met.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-rose-100 border border-rose-300 rounded-xl p-4">
|
||||
<p className="text-xs text-slate-700">
|
||||
<span className="font-bold text-rose-900">
|
||||
The key insight:
|
||||
</span>{" "}
|
||||
These two criteria work independently. You can fail on goal
|
||||
while passing accuracy. You can fail on accuracy while passing
|
||||
goal. Only the answer that passes both is correct.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* §9.2 — Question Anatomy */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Question Anatomy
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Every Rhetorical Synthesis question has the same four-part
|
||||
structure. Each part plays a specific role in determining the
|
||||
correct answer.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse rounded-xl overflow-hidden">
|
||||
<thead>
|
||||
<tr className="bg-rose-600 text-white">
|
||||
<th className="px-3 py-2 text-left text-xs">Component</th>
|
||||
<th className="px-3 py-2 text-left text-xs">
|
||||
What It Contains
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs">
|
||||
What You Need From It
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
[
|
||||
"Context Header",
|
||||
'"While researching a topic, a student took the following notes:"',
|
||||
"Sets the topic domain. Establishes what field the notes come from.",
|
||||
],
|
||||
[
|
||||
"Notes (2–4 bullets)",
|
||||
"Factual information: data, definitions, examples, findings",
|
||||
"The ONLY source of accurate facts. Every claim in the correct answer must trace back here.",
|
||||
],
|
||||
[
|
||||
"Stated Goal",
|
||||
'"The student wants to [X]. Which choice best accomplishes this goal?"',
|
||||
"Read this FIRST — before notes, before answer choices. It tells you exactly what the correct answer must do.",
|
||||
],
|
||||
[
|
||||
"Four Answer Choices",
|
||||
"Sentences combining facts from the notes",
|
||||
"Wrong answers fail the goal, distort facts, add outside information, or combine incompatible notes.",
|
||||
],
|
||||
].map(([comp, contains, need], i) => (
|
||||
<tr
|
||||
key={comp}
|
||||
className={i % 2 === 0 ? "bg-white" : "bg-rose-50"}
|
||||
>
|
||||
<td className="px-3 py-2 font-bold text-rose-800 text-xs whitespace-nowrap">
|
||||
{comp}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-600 text-xs italic">
|
||||
{contains}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-slate-700 text-xs">
|
||||
{need}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 1: Goals & Strategy ── */}
|
||||
<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">
|
||||
Goals & Strategy
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
The eight rhetorical goals, the 5-step method, and the six ways
|
||||
wrong answers fail.
|
||||
</p>
|
||||
|
||||
{/* §9.3 — The 8 Goals */}
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-rose-50 border border-rose-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-rose-900">
|
||||
The 8 Rhetorical Goals — tap to reveal what each answer must do:
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
The stated goal will always be one of these eight types. Knowing
|
||||
them lets you immediately identify what structural features the
|
||||
correct answer must contain.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={RHETORICAL_GOALS}
|
||||
columns={4}
|
||||
accentColor="rose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* §9.5 — The 5-Step Strategy */}
|
||||
<div className="scroll-reveal stagger-2 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
The 5-Step Strategy
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
action: "Read the GOAL first",
|
||||
tip: 'Before reading notes or answer choices, read the stated goal. Write the goal type in 3–4 words on scratch paper: e.g., "comparison — key difference." The goal determines everything else.',
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Read the notes with the goal in mind",
|
||||
tip: "Identify which 1–2 notes are relevant to your goal. Cross out off-topic notes — they exist only as material for wrong answers.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Predict what the correct answer must contain",
|
||||
tip: 'Before looking at choices, write a quick sketch: "must mention both species + a difference." Even a rough prediction blocks you from being swayed by sophisticated wrong answers.',
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Eliminate wrong goals first",
|
||||
tip: "Any answer that achieves a DIFFERENT goal from the one stated is wrong — eliminate it before checking any facts. This alone usually removes 2–3 choices immediately.",
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify accuracy in the remaining answers",
|
||||
tip: "Read each remaining answer word by word against the notes. One wrong number, reversed relationship, or unsupported claim eliminates the answer.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.step}
|
||||
className="flex gap-3 bg-rose-50 border border-rose-100 rounded-xl px-4 py-3"
|
||||
>
|
||||
<span className="w-6 h-6 rounded-full bg-rose-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{s.step}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 text-sm">
|
||||
{s.action}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{s.tip}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* §9.4 — Wrong Answer Patterns */}
|
||||
<div className="scroll-reveal stagger-3 rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Wrong Answer Patterns — tap to reveal each trap:
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Every wrong answer fails in one of these six predictable ways.
|
||||
Naming the failure pattern immediately tells you why to eliminate
|
||||
it.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={WRONG_PATTERNS}
|
||||
columns={3}
|
||||
accentColor="rose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="scroll-reveal-scale golden-rule-glow bg-rose-900 text-white rounded-2xl p-5 mb-8">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-rose-100">
|
||||
Read the GOAL before reading anything else. An accurate answer
|
||||
that achieves the wrong goal is wrong. An answer that achieves the
|
||||
right goal with one distorted fact is also wrong. Both criteria
|
||||
must be met.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 2: Worked Example ── */}
|
||||
<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-2">
|
||||
Worked Example
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
A complete question with full analysis of why each answer succeeds
|
||||
or fails against the two criteria.
|
||||
</p>
|
||||
|
||||
<div className="scroll-reveal stagger-1 rounded-2xl p-6 mb-8 bg-slate-50 border border-slate-200 space-y-4">
|
||||
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||
While researching a topic, a student took the following notes:
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
"Komodo dragons (Varanus komodoensis) are the world's largest living lizard species, reaching up to 3 meters in length.",
|
||||
"Komodo dragons are found only on five islands in Indonesia: Komodo, Rinca, Flores, Gili Motang, and Padar.",
|
||||
"Komodo dragons have a venomous bite that prevents blood clotting in prey, causing prey to weaken from blood loss.",
|
||||
"The IUCN lists Komodo dragons as Endangered, with an estimated 1,383–2,531 individuals remaining in the wild.",
|
||||
"Nile monitor lizards, native to Africa, are also large lizards but are not venomous and grow to only 2 meters.",
|
||||
].map((note, i) => (
|
||||
<li key={i} className="flex gap-2 text-sm text-slate-700">
|
||||
<span className="text-rose-500 font-bold shrink-0">
|
||||
{i + 1}.
|
||||
</span>
|
||||
{note}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="bg-rose-100 border border-rose-300 rounded-xl p-3">
|
||||
<p className="text-sm font-bold text-rose-900">
|
||||
The student wants to COMPARE the Komodo dragon with another
|
||||
large lizard species by highlighting a key difference. Which
|
||||
choice best accomplishes this goal?
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
letter: "A",
|
||||
correct: false,
|
||||
text: "Komodo dragons, the world's largest living lizards at 3 meters, are endangered, with fewer than 2,531 individuals remaining.",
|
||||
analysis:
|
||||
"✗ Wrong goal. Only one species is mentioned — no comparison is made. The goal explicitly requires comparing Komodo dragons WITH another species.",
|
||||
},
|
||||
{
|
||||
letter: "B",
|
||||
correct: true,
|
||||
text: "Unlike Nile monitor lizards, which are not venomous and reach only 2 meters, Komodo dragons possess a venomous bite and can grow up to 3 meters.",
|
||||
analysis:
|
||||
"✓ Correct. Both species appear. A key difference is highlighted (venomous vs. not venomous, 3m vs. 2m). All facts trace directly to Notes 1 and 5. Goal ✓, Accuracy ✓.",
|
||||
},
|
||||
{
|
||||
letter: "C",
|
||||
correct: false,
|
||||
text: "Komodo dragons are venomous lizards found on five Indonesian islands, and their numbers have declined to fewer than 2,531 in the wild.",
|
||||
analysis:
|
||||
"✗ Wrong goal. Only one species mentioned — no comparison. The facts are accurate, but the goal is not achieved.",
|
||||
},
|
||||
{
|
||||
letter: "D",
|
||||
correct: false,
|
||||
text: "Both Komodo dragons and Nile monitor lizards are large reptiles, with Komodo dragons endangered and Nile monitors thriving across Africa.",
|
||||
analysis:
|
||||
'✗ Two failures. First, the goal asks for a KEY DIFFERENCE, not similarity — "both are large reptiles" emphasizes similarity. Second, "Nile monitors thriving" appears in no note — outside information.',
|
||||
},
|
||||
].map((opt) => (
|
||||
<div
|
||||
key={opt.letter}
|
||||
className={`rounded-xl p-4 border ${opt.correct ? "bg-green-50 border-green-300" : "bg-red-50 border-red-200"}`}
|
||||
>
|
||||
<div className="flex gap-3 items-start">
|
||||
<span
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold shrink-0 ${opt.correct ? "bg-green-600 text-white" : "bg-red-500 text-white"}`}
|
||||
>
|
||||
{opt.letter}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm text-slate-800 italic mb-2">
|
||||
"{opt.text}"
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">{opt.analysis}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 text-sm mb-1">Takeaways</p>
|
||||
<ul className="space-y-1 text-xs text-slate-700">
|
||||
<li>
|
||||
• "Compare by highlighting a key difference" = two species
|
||||
must appear AND a difference must be stated.
|
||||
</li>
|
||||
<li>
|
||||
• A and C both used accurate facts — but both failed the goal
|
||||
test. Eliminated without checking accuracy.
|
||||
</li>
|
||||
<li>
|
||||
• D failed on both criteria: wrong goal type (similarity
|
||||
instead of difference) AND outside information.
|
||||
</li>
|
||||
<li>
|
||||
• B is the only answer that passes both criteria: goal ✓ (two
|
||||
species, key difference stated), accuracy ✓ (all facts from
|
||||
Notes 1 and 5).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 3: Goal Matcher Widget ── */}
|
||||
<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-2">
|
||||
Goal Matcher
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Find the sentence that best achieves the stated rhetorical goal
|
||||
using only facts from the notes.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="rose"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ── Section 4: Practice ── */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</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-rose-900 text-white font-bold rounded-full hover:bg-rose-700 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWRhetoricalSynthesisLesson;
|
||||
@ -219,7 +219,7 @@ const EBRWSemicolonsColonsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
@ -244,7 +244,7 @@ const EBRWSentenceStructureLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
@ -233,7 +233,7 @@ const EBRWSubjectVerbLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
941
src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx
Normal file
941
src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx
Normal file
@ -0,0 +1,941 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Check, BookOpen, Lightbulb, Zap, Star, BarChart3 } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
TEXT_STRUCTURE_EASY,
|
||||
TEXT_STRUCTURE_MEDIUM,
|
||||
} from "../../../data/rw/text-structure-purpose";
|
||||
import EvidenceHunterWidget, {
|
||||
type EvidenceExercise,
|
||||
} from "../../../components/lessons/EvidenceHunterWidget";
|
||||
import DataClaimWidget, {
|
||||
type DataExercise,
|
||||
} from "../../../components/lessons/DataClaimWidget";
|
||||
import RevealCardGrid from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const EVIDENCE_EXERCISES: EvidenceExercise[] = [
|
||||
{
|
||||
question: "What is the main point of this passage?",
|
||||
passage: [
|
||||
"Scientists have long assumed that deep-sea creatures live in perpetual darkness.",
|
||||
"However, a 2023 study by marine biologist Dr. Yuen found bioluminescent signals in fish previously believed to be blind.",
|
||||
"The fish not only detect light but appear to use it for communication.",
|
||||
"These findings suggest that vision may be far more widespread in the deep ocean than previously thought.",
|
||||
"Future expeditions will need to reconsider how they classify deep-sea species.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
'Sentence 4 states the "So What" — the surprising new conclusion about how widespread vision is. It is the main point, not a supporting detail. The word "suggest" introduces the author\'s key takeaway.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Which sentence best identifies the author's PRIMARY PURPOSE in writing this passage?",
|
||||
passage: [
|
||||
"The Harlem Renaissance of the 1920s produced some of the most innovative jazz in American history.",
|
||||
"Composers like Duke Ellington and Louis Armstrong transformed the harmonic language of popular music.",
|
||||
"Yet historians have traditionally focused on the literary figures of this era while neglecting the musicians.",
|
||||
"This essay argues that jazz musicians were the true cultural architects of the Harlem Renaissance.",
|
||||
"By examining their influence on poetry and visual art, we can restore them to their rightful place in history.",
|
||||
],
|
||||
evidenceIndex: 3,
|
||||
explanation:
|
||||
'Sentence 4 contains the thesis — "This essay argues" is a classic purpose-signaling phrase that tells you both the PURPOSE (to argue) and the CLAIM (jazz musicians were the true architects). The other sentences provide context or support.',
|
||||
},
|
||||
];
|
||||
|
||||
const GRAPH_EXERCISES: DataExercise[] = [
|
||||
{
|
||||
title: "Bar — Book Format Preferences",
|
||||
chart: {
|
||||
type: "bar",
|
||||
title: "Preferred Reading Format by Age Group (% of respondents)",
|
||||
yLabel: "% of respondents",
|
||||
xLabel: "Age Group",
|
||||
unit: "%",
|
||||
source: "National Reading Survey, 2023",
|
||||
series: [
|
||||
{
|
||||
name: "Print Books",
|
||||
data: [
|
||||
{ label: "18–24", value: 32 },
|
||||
{ label: "25–34", value: 41 },
|
||||
{ label: "35–49", value: 55 },
|
||||
{ label: "50–64", value: 68 },
|
||||
{ label: "65+", value: 78 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "E-books",
|
||||
data: [
|
||||
{ label: "18–24", value: 48 },
|
||||
{ label: "25–34", value: 39 },
|
||||
{ label: "35–49", value: 30 },
|
||||
{ label: "50–64", value: 22 },
|
||||
{ label: "65+", value: 12 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "Preference for print books increases with age across all groups shown.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"Print preference rises at each age bracket: 32% → 41% → 55% → 68% → 78%. Every step increases, so this is directly supported.",
|
||||
},
|
||||
{
|
||||
text: "Adults aged 65+ prefer print books because they find e-readers difficult to use.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The graph shows WHAT people prefer, not WHY. Difficulty with e-readers is a plausible explanation, but nothing in the data addresses reasons for preference.",
|
||||
},
|
||||
{
|
||||
text: "E-books are more popular than print books among every age group under 35.",
|
||||
verdict: "contradicted",
|
||||
explanation:
|
||||
'Ages 18–24: E-books (48%) > Print (32%) — true. But ages 25–34: E-books (39%) < Print (41%) — false. Since the claim says "every age group under 35," one counterexample is enough to contradict it. Always check ALL data points when a claim uses "every" or "all."',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Line — Library Visits",
|
||||
chart: {
|
||||
type: "line",
|
||||
title: "Monthly Library Visits per Capita (2018–2023)",
|
||||
yLabel: "Visits per capita",
|
||||
xLabel: "Year",
|
||||
unit: "",
|
||||
source: "American Library Association Annual Report",
|
||||
series: [
|
||||
{
|
||||
name: "In-Person Visits",
|
||||
data: [
|
||||
{ label: "2018", value: 4.2 },
|
||||
{ label: "2019", value: 4.0 },
|
||||
{ label: "2020", value: 1.1 },
|
||||
{ label: "2021", value: 2.3 },
|
||||
{ label: "2022", value: 3.1 },
|
||||
{ label: "2023", value: 3.4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Digital Access",
|
||||
data: [
|
||||
{ label: "2018", value: 1.8 },
|
||||
{ label: "2019", value: 2.1 },
|
||||
{ label: "2020", value: 5.6 },
|
||||
{ label: "2021", value: 4.9 },
|
||||
{ label: "2022", value: 4.5 },
|
||||
{ label: "2023", value: 4.3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
claims: [
|
||||
{
|
||||
text: "Digital access surpassed in-person visits starting in 2020 and remained higher through 2023.",
|
||||
verdict: "supported",
|
||||
explanation:
|
||||
"In 2020, Digital (5.6) > In-Person (1.1). This pattern continues: 2021 (4.9 > 2.3), 2022 (4.5 > 3.1), 2023 (4.3 > 3.4). Directly supported throughout every year from 2020 onward.",
|
||||
},
|
||||
{
|
||||
text: "The pandemic caused the decline in in-person library visits in 2020.",
|
||||
verdict: "neither",
|
||||
explanation:
|
||||
"The graph shows a sharp drop in in-person visits in 2020, but the data itself does not identify the CAUSE. While the pandemic is a plausible explanation, the graph provides no information about causes — only trends.",
|
||||
},
|
||||
{
|
||||
text: "In-person visits in 2023 had fully recovered to pre-2020 levels.",
|
||||
verdict: "contradicted",
|
||||
explanation:
|
||||
"2023 in-person visits = 3.4. Pre-2020 levels were 4.2 (2018) and 4.0 (2019). Since 3.4 < 4.0, visits had NOT fully recovered. Directly contradicted.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const FUNCTION_WORDS = [
|
||||
{
|
||||
label: "Positive / Supportive",
|
||||
content:
|
||||
"advance, affirm, bolster, defend, exemplify, illustrate, provide evidence for, substantiate, praise, propose, support",
|
||||
},
|
||||
{
|
||||
label: "Negative / Critical",
|
||||
content:
|
||||
"challenge, contradict, criticize, debate, deny, dismiss, question, refute, undermine, warn, raise concerns about",
|
||||
},
|
||||
{
|
||||
label: "Neutral — Describe",
|
||||
content:
|
||||
"characterize, convey, depict, describe, discuss, dramatize, portray, present, represent, show, trace",
|
||||
},
|
||||
{
|
||||
label: "Neutral — Emphasize",
|
||||
content:
|
||||
"call attention to, emphasize, highlight, reinforce, reiterate, underscore",
|
||||
},
|
||||
{
|
||||
label: "Neutral — Explain",
|
||||
content:
|
||||
"account for, clarify, define, explicate, explain, identify, indicate, specify",
|
||||
},
|
||||
{
|
||||
label: "Neutral — Analyze",
|
||||
content:
|
||||
"analyze, consider, develop, explore, hypothesize, imply, reflect on, speculate, suggest",
|
||||
},
|
||||
{
|
||||
label: "Compare / Contrast",
|
||||
content: "compare, contrast, distinguish between, draw a parallel between",
|
||||
},
|
||||
{
|
||||
label: "Qualify",
|
||||
content:
|
||||
"acknowledge, concede, downplay, minimize, qualify — note: qualify means limit or refine, not verify",
|
||||
},
|
||||
];
|
||||
|
||||
const WRONG_ANSWER_PATTERNS = [
|
||||
{
|
||||
label: "Off-topic",
|
||||
content:
|
||||
"Mentions something real but not discussed in the passage or relevant lines",
|
||||
},
|
||||
{
|
||||
label: "Too broad",
|
||||
content:
|
||||
"Passage discusses one specific thing; answer generalizes to the whole category",
|
||||
},
|
||||
{
|
||||
label: "Too extreme",
|
||||
content:
|
||||
"Includes words like always, never, only, completely, impossible, proven",
|
||||
},
|
||||
{
|
||||
label: "Half-right, half-wrong",
|
||||
content:
|
||||
"Contains accurate language but also one incorrect element — one wrong word kills the whole answer",
|
||||
},
|
||||
{
|
||||
label: "Could be true",
|
||||
content:
|
||||
"Plausible or factually accurate, but not supported by the specific passage",
|
||||
},
|
||||
{
|
||||
label: "Passage-true but not local",
|
||||
content:
|
||||
"Accurate for the passage as a whole but not for the specific lines referenced",
|
||||
},
|
||||
];
|
||||
|
||||
const EBRWTextStructurePurposeLesson: 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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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="Pronouns & Main Point"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={1}
|
||||
title="Old/New & Passage Types"
|
||||
icon={Star}
|
||||
/>
|
||||
<SectionMarker
|
||||
index={2}
|
||||
title="Function & Evidence"
|
||||
icon={BarChart3}
|
||||
/>
|
||||
<SectionMarker index={3} title="Passage Hunter" icon={Lightbulb} />
|
||||
<SectionMarker index={4} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 w-full mx-auto">
|
||||
{/* ── SECTION 0: PRONOUNS & MAIN POINT ── */}
|
||||
<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 w-fit">
|
||||
Craft & Structure — Domain 1
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Text Structure & Purpose
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Understanding not just what a passage says but how it is organized
|
||||
and why — the two foundational skills are tracking references and
|
||||
finding the main point.
|
||||
</p>
|
||||
|
||||
{/* Pronouns & Compression Nouns */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Pronouns & Compression Nouns
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
SAT passages routinely use pronouns (it, they, this, that, these)
|
||||
and abstract "compression" nouns (phenomenon, claim, notion,
|
||||
approach) to refer back to ideas already introduced. Inability to
|
||||
trace these references is a major source of comprehension error.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Pronoun Rules
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Always back up to find the referent — it may be in a previous sentence or paragraph.",
|
||||
"Match singular pronouns to singular referents; plural pronouns to plural referents.",
|
||||
"When multiple nouns of the same type appear, a pronoun becomes ambiguous — re-read carefully.",
|
||||
"It and its can refer to different nouns in the same paragraph — track each separately.",
|
||||
].map((r) => (
|
||||
<li
|
||||
key={r}
|
||||
className="flex items-start gap-2 text-xs text-slate-600"
|
||||
>
|
||||
<span className="text-fuchsia-500 shrink-0 mt-0.5">
|
||||
•
|
||||
</span>
|
||||
{r}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Compression Nouns
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
Words like{" "}
|
||||
<em>
|
||||
phenomenon, observation, claim, assertion, notion, approach,
|
||||
finding
|
||||
</em>{" "}
|
||||
compress an entire idea into one phrase.
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
'"this + noun": "this enhanced convenience" compresses the entire previous sentence.',
|
||||
'"the former / the latter": former = first mentioned; latter = last mentioned.',
|
||||
"The referent may be 3–5 lines before the compression noun — always read backwards.",
|
||||
].map((r) => (
|
||||
<li
|
||||
key={r}
|
||||
className="flex items-start gap-2 text-xs text-slate-600"
|
||||
>
|
||||
<span className="text-fuchsia-500 shrink-0 mt-0.5">
|
||||
•
|
||||
</span>
|
||||
{r}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-fuchsia-100 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-bold text-fuchsia-800">Strategy: </span>
|
||||
When you encounter a pronoun or compression noun, stop. Back up
|
||||
to the beginning of the sentence where the referent was
|
||||
introduced. Never start reading mid-sentence when searching for
|
||||
a referent.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Topics & Main Point */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Identifying Topics and Main Points
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-slate-700 text-sm mb-1">
|
||||
The Topic
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
The primary subject or focus of the passage — the person,
|
||||
thing, or idea it centers on. Typically appears in the first
|
||||
sentence or two.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 italic">
|
||||
Correct answers to main-idea questions always refer to the
|
||||
topic. Many wrong answers are wrong specifically because they
|
||||
are off-topic.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-1">
|
||||
The Main Point
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
The primary argument the author wants to convey. It answers:{" "}
|
||||
<em>so what?</em> It tells us why the topic matters.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-xs bg-fuchsia-600 text-white px-2 py-0.5 rounded font-bold">
|
||||
Topic + So What? = Main Point
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-slate-700 text-sm mb-2">
|
||||
Where to Find the Main Point
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{[
|
||||
"Most commonly in the first sentence or two",
|
||||
"Often restated or reinforced in the last sentence",
|
||||
"Signaled by: the point is, key, central, essential, significant, or italics",
|
||||
"Often located near dashes, colons, or major transitions (however, therefore, in fact)",
|
||||
].map((p) => (
|
||||
<div
|
||||
key={p}
|
||||
className="flex items-start gap-2 text-xs text-slate-600"
|
||||
>
|
||||
<span className="text-fuchsia-500 shrink-0 mt-0.5">✓</span>
|
||||
{p}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
|
||||
<p className="text-xs text-slate-600">
|
||||
<span className="font-bold text-slate-800">
|
||||
What to write down:{" "}
|
||||
</span>
|
||||
A 3–6 word compressed summary on scratch paper. Example: "New
|
||||
evidence overturns Clovis theory." This prevents you from
|
||||
forgetting the point when navigating confusing answer choices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 1: OLD/NEW & PASSAGE TYPES ── */}
|
||||
<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">
|
||||
Old/New & Passage Types
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
The most important structural pattern in SAT science passages — and
|
||||
how main points differ across fiction, non-fiction, and poetry.
|
||||
</p>
|
||||
|
||||
{/* Old/New Framework */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
The Old Idea / New Idea Framework
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
One of the most important structural patterns in SAT passages:
|
||||
people used to believe X, but new evidence shows Y is actually
|
||||
true. Recognizing old-idea signals allows you to predict the main
|
||||
point before the author states it.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-700 text-sm mb-2">
|
||||
Old Idea Signal Phrases
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
'"Some / many (scientists) believe..." — "some" especially implies the author disagrees',
|
||||
'"It is commonly thought that..." / "Accepted wisdom holds that..."',
|
||||
'"Traditionally it was believed..." / "For decades, scientists thought..."',
|
||||
'"In the past..."',
|
||||
].map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="text-xs text-slate-600 flex items-start gap-1"
|
||||
>
|
||||
<span className="shrink-0">•</span>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p className="font-bold text-green-700 text-sm mb-2">
|
||||
New Idea Signal Phrases
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"However, but in fact, actually, in reality",
|
||||
'"It now seems / researchers now think..."',
|
||||
'"Recently, it has been found that..." / "New research suggests..."',
|
||||
'"But is it really the case that...?" (rhetorical question)',
|
||||
].map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="text-xs text-slate-600 flex items-start gap-1"
|
||||
>
|
||||
<span className="shrink-0">•</span>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-fuchsia-900 text-white rounded-xl p-4">
|
||||
<p className="font-bold text-sm mb-1">Strategy</p>
|
||||
<p className="text-xs text-fuchsia-100">
|
||||
Write <em>Old = [3-word summary] | New = [3-word summary]</em>{" "}
|
||||
on scratch paper before answering questions. The main point is
|
||||
almost always the NEW idea. If you identify the old idea, you
|
||||
can predict the new idea before reading it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Passage Types */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Main Point for Different Passage Types
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 card-tilt">
|
||||
<p className="font-bold text-slate-700 text-sm mb-1">
|
||||
Non-Fiction (Science, Social Science, Humanities)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Main points are usually stated directly, often in the first
|
||||
and last sentences. The old idea / new idea framework applies
|
||||
most frequently here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 card-tilt">
|
||||
<p className="font-bold text-slate-700 text-sm mb-2">
|
||||
Fiction Passages
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
Fiction passages do not make arguments but focus on particular
|
||||
traits, qualities, or situations of characters or places. Key
|
||||
information tends to appear at the beginning and end of the
|
||||
excerpt.
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Do not interpret or speculate beyond what is literally stated.",
|
||||
"Focus on the literal actions of characters and the specific words describing them.",
|
||||
"Pay attention to unusual punctuation, strong language, and major transitions.",
|
||||
"Characters' speech often signals the key idea, especially near the end.",
|
||||
].map((r) => (
|
||||
<li
|
||||
key={r}
|
||||
className="flex items-start gap-2 text-xs text-slate-500"
|
||||
>
|
||||
<span className="text-fuchsia-400 shrink-0 mt-0.5">
|
||||
•
|
||||
</span>
|
||||
{r}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 card-tilt">
|
||||
<p className="font-bold text-slate-700 text-sm mb-2">
|
||||
Poetry Passages
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
The literal meaning may be conveyed indirectly through images
|
||||
or metaphors, but students must stay within the bounds of the
|
||||
poem and avoid over-interpretation.
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Identify the two main sections (often divided at the midpoint).",
|
||||
"Determine what each section is about at the most literal level.",
|
||||
"Look for the contrast or development between sections — that relationship usually contains the main idea.",
|
||||
"Avoid attributing complex philosophical meaning unless unambiguously supported by specific words.",
|
||||
].map((r) => (
|
||||
<li
|
||||
key={r}
|
||||
className="flex items-start gap-2 text-xs text-slate-500"
|
||||
>
|
||||
<span className="text-fuchsia-400 shrink-0 mt-0.5">
|
||||
•
|
||||
</span>
|
||||
{r}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 2: FUNCTION, EVIDENCE & GRAPHS ── */}
|
||||
<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-2">
|
||||
Function, Evidence & Graphs
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Three advanced question types — all require identifying the claim
|
||||
precisely before answering, then playing positive/negative to
|
||||
eliminate.
|
||||
</p>
|
||||
|
||||
{/* Reading for Function */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Reading for Function
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
Function questions ask not <em>what</em> a sentence says but{" "}
|
||||
<em>why</em> it says it — what rhetorical role that content plays.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-fuchsia-100 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Main Point vs. Primary Purpose
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-fuchsia-700">
|
||||
Main Point
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The central CLAIM — what the author is arguing. Usually
|
||||
stated directly.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-fuchsia-700">
|
||||
Primary Purpose
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
The rhetorical GOAL — what the author is doing
|
||||
(explaining, emphasizing, arguing). Often more general or
|
||||
abstract.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-fuchsia-100 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
5-Step Method for Function Questions
|
||||
</p>
|
||||
<ol className="space-y-1">
|
||||
{[
|
||||
"Determine whether the question asks about a sentence or the whole passage.",
|
||||
"Read one to two sentences before and after the underlined portion.",
|
||||
"Ask: What point is this sentence supporting?",
|
||||
"Play positive/negative — eliminate answers with the wrong charge.",
|
||||
"Match the remaining choice to what the passage is actually doing.",
|
||||
].map((step, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 text-xs text-slate-600"
|
||||
>
|
||||
<span className="w-4 h-4 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-xs shrink-0 mt-0.5">
|
||||
{i + 1}
|
||||
</span>
|
||||
{step}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<RevealCardGrid
|
||||
cards={FUNCTION_WORDS}
|
||||
columns={2}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-xs text-red-800">
|
||||
Eliminate extreme verbs immediately:{" "}
|
||||
<em>prove, celebrate, condemn, discredit, mock, scoff at</em>.
|
||||
Passages almost never contain sufficient evidence to
|
||||
definitively prove or disprove anything.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Completions & Support/Undermine */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Text Completions & Support / Undermine
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Text Completions
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
All necessary information is present in the passage. The
|
||||
inference required is small — resist speculation.
|
||||
</p>
|
||||
<ol className="space-y-1">
|
||||
{[
|
||||
"Carefully read the claim just before the blank.",
|
||||
"Restate it in your own words (3–6 words).",
|
||||
"Work out the single most logical implication.",
|
||||
"Find the answer that matches — possibly rephrased.",
|
||||
].map((s, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs text-slate-700">
|
||||
<span className="w-4 h-4 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-xs shrink-0">
|
||||
{i + 1}
|
||||
</span>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-2">
|
||||
Support & Undermine
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
Students must identify statements that strengthen or weaken a
|
||||
given claim. These tend to appear among the hardest questions.
|
||||
</p>
|
||||
<ol className="space-y-1">
|
||||
{[
|
||||
"Identify and paraphrase the claim precisely.",
|
||||
"Predict upfront what kind of information would support or weaken it.",
|
||||
"Match your prediction — wrong answers will be off-topic or address a different claim.",
|
||||
].map((s, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs text-slate-700">
|
||||
<span className="w-4 h-4 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-xs shrink-0">
|
||||
{i + 1}
|
||||
</span>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<p className="font-bold text-green-800 text-sm mb-1">Support</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Correct answer provides evidence consistent with the claim —
|
||||
same entities or phenomena, results in the same direction. Not
|
||||
off-topic, not tangential.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="font-bold text-red-800 text-sm mb-1">Undermine</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Correct answer contradicts or weakens the claim. Most common
|
||||
pattern: claim says "X causes Y"; correct answer shows X is
|
||||
present but Y did not follow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Graphs & Answer Patterns */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal stagger-2">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Graphs, Charts & Wrong Answer Patterns
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
Graph-based questions follow predictable patterns and can often be
|
||||
answered without extensively examining the graph.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
text: "Read the passage first — especially the last sentence. This states the claim.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
text: "Read the question carefully to identify the exact focus.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
text: "Eliminate answer choices using the passage before looking at the graph.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
text: "Confirm the remaining option against the graph.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.step}
|
||||
className="flex gap-3 bg-white border border-fuchsia-100 rounded-lg p-3"
|
||||
>
|
||||
<span className="w-5 h-5 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{s.step}
|
||||
</span>
|
||||
<p className="text-xs text-slate-700">{s.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<RevealCardGrid
|
||||
cards={WRONG_ANSWER_PATTERNS}
|
||||
columns={2}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Graph Practice */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Graph Practice — Evaluate Claims Against Data
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Apply the 4-step process: read the claim, extract criteria, check
|
||||
the graph, and judge whether the data supports, contradicts, or
|
||||
neither proves the claim.
|
||||
</p>
|
||||
<DataClaimWidget
|
||||
exercises={GRAPH_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-fuchsia-900 text-white rounded-2xl p-5 mb-8 scroll-reveal-scale golden-rule-glow">
|
||||
<p className="font-bold mb-1">Golden Rule</p>
|
||||
<p className="text-sm text-fuchsia-100">
|
||||
The main point is almost always the sentence that introduces NEW
|
||||
information — look for signal words like "however," "but," or "in
|
||||
contrast." The OLD information (background, history, common
|
||||
belief) is context, not the point. For function questions: play
|
||||
positive/negative first, then match the function verb to what the
|
||||
passage is actually doing.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 3: WIDGET ── */}
|
||||
<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-2">
|
||||
Passage Hunter
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Click the sentence that best answers each question. Look for the "So
|
||||
What" and purpose signal words.
|
||||
</p>
|
||||
<EvidenceHunterWidget
|
||||
exercises={EVIDENCE_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 4: PRACTICE ── */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</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 EBRWTextStructurePurposeLesson;
|
||||
File diff suppressed because it is too large
Load Diff
@ -241,7 +241,7 @@ const EBRWVerbsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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
|
||||
|
||||
@ -185,7 +185,7 @@ const EBRWVocabMeaningLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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}
|
||||
|
||||
@ -185,7 +185,7 @@ const EBRWVocabPreciseLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
|
||||
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 lg:bg-transparent z-0 hidden lg:block">
|
||||
<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}
|
||||
|
||||
902
src/pages/student/lessons/EBRWWordsInContextLesson.tsx
Normal file
902
src/pages/student/lessons/EBRWWordsInContextLesson.tsx
Normal file
@ -0,0 +1,902 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Check, BookOpen, Lightbulb, Zap, Star, Target } from "lucide-react";
|
||||
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
|
||||
import {
|
||||
WORDS_CONTEXT_EASY,
|
||||
WORDS_CONTEXT_MEDIUM,
|
||||
} from "../../../data/rw/words-in-context";
|
||||
import ContextEliminationWidget, {
|
||||
type VocabExercise,
|
||||
} from "../../../components/lessons/ContextEliminationWidget";
|
||||
import RevealCardGrid from "../../../components/lessons/RevealCardGrid";
|
||||
import useScrollReveal from "../../../components/lessons/useScrollReveal";
|
||||
|
||||
interface LessonProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
const VOCAB_EXERCISES: VocabExercise[] = [
|
||||
{
|
||||
sentence:
|
||||
"The documentary filmmaker hoped to ______ audiences with her unflinching portrayal of poverty, prompting them to advocate for social change.",
|
||||
word: "______",
|
||||
question: "Which word most logically completes the sentence?",
|
||||
options: [
|
||||
{
|
||||
id: "a",
|
||||
definition: "entertain — to provide amusement",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"The context signals a serious goal (advocacy, social change), not mere amusement.",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
definition: "galvanize — to shock or excite into taking action",
|
||||
isCorrect: true,
|
||||
elimReason: "",
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
definition: "placate — to make less angry or upset",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Contradicts the goal of prompting advocacy; placating suggests calming, not motivating.",
|
||||
},
|
||||
{
|
||||
id: "d",
|
||||
definition: "perplex — to confuse someone greatly",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Confusing audiences would undermine the filmmaker's goal of advocacy.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"Though often described as a recluse, the novelist was in fact quite ______, hosting weekly salons attended by scientists, poets, and politicians.",
|
||||
word: "______",
|
||||
question: "Which word most logically completes the sentence?",
|
||||
options: [
|
||||
{
|
||||
id: "a",
|
||||
definition: "gregarious — fond of company; sociable",
|
||||
isCorrect: true,
|
||||
elimReason: "",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
definition: "reticent — not revealing thoughts or feelings",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
'Reticent reinforces "recluse" rather than contrasting with it; the word "though" signals a contradiction.',
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
definition: "pensive — engaged in deep thought",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Being thoughtful does not explain weekly social gatherings.",
|
||||
},
|
||||
{
|
||||
id: "d",
|
||||
definition: "fastidious — very attentive to detail",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Attention to detail is unrelated to the sociability implied by hosting salons.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sentence:
|
||||
"The economist's forecast proved remarkably ______: every indicator she had cited came to pass within two years.",
|
||||
word: "______",
|
||||
question: "Which word most logically completes the sentence?",
|
||||
options: [
|
||||
{
|
||||
id: "a",
|
||||
definition: "optimistic — hopeful about the future",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Optimistic describes an attitude, not the accuracy of a forecast.",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
definition: "prescient — having knowledge of events before they occur",
|
||||
isCorrect: true,
|
||||
elimReason: "",
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
definition: "contentious — causing disagreement",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
'The colon and "every indicator came to pass" signals accuracy, not controversy.',
|
||||
},
|
||||
{
|
||||
id: "d",
|
||||
definition: "redundant — not or no longer needed; superfluous",
|
||||
isCorrect: false,
|
||||
elimReason:
|
||||
"Redundancy contradicts the idea that every prediction proved true.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const CLAIM_VERBS = [
|
||||
{
|
||||
label: "Make a claim",
|
||||
words: "advance, hypothesize, posit, proffer, speculate",
|
||||
},
|
||||
{ label: "Support a claim", words: "bolster, buttress, substantiate" },
|
||||
{
|
||||
label: "Question a claim",
|
||||
words: "rebut, refute, ambivalence, skepticism",
|
||||
},
|
||||
{ label: "Draw a conclusion", words: "infer, surmise" },
|
||||
{ label: "Large amount", words: "multitude, plethora, profusion" },
|
||||
{ label: "Small amount", words: "dearth, paucity" },
|
||||
{ label: "Different/diverse", words: "disparate, eclectic, heterogeneous" },
|
||||
{ label: "Noticeable/striking", words: "conspicuous, distinctive, salient" },
|
||||
{ label: "Inborn", words: "inherent, innate, intrinsic" },
|
||||
];
|
||||
|
||||
const SCIENCE_TERMS = [
|
||||
{ word: "empirical", def: "Relying on hard, observable data" },
|
||||
{ word: "hierarchy", def: "System of rank; opposite of egalitarian" },
|
||||
{ word: "indigenous", def: "Native to a region" },
|
||||
{ word: "inhibit", def: "To prevent or impede a response" },
|
||||
{ word: "predisposed", def: "Having a tendency toward something" },
|
||||
{ word: "replicate", def: "To repeat an experiment with same results" },
|
||||
{ word: "stimulus", def: "Something that provokes a specific response" },
|
||||
{ word: "symbiotic", def: "Beneficial interaction between two organisms" },
|
||||
{ word: "taxonomy", def: "A classification system" },
|
||||
{ word: "velocity", def: "Speed" },
|
||||
];
|
||||
|
||||
const ALL_VOCAB_KEYS = [
|
||||
...CLAIM_VERBS.map((v) => v.label),
|
||||
...SCIENCE_TERMS.map((t) => t.word),
|
||||
];
|
||||
|
||||
const SECOND_MEANINGS = [
|
||||
{ label: "arrest", content: "To stop (not just apprehend a criminal)" },
|
||||
{
|
||||
label: "assume",
|
||||
content: "To take on responsibility for; to acquire a new position",
|
||||
},
|
||||
{
|
||||
label: "bent",
|
||||
content:
|
||||
"A liking or natural tendency toward something (synonym: proclivity)",
|
||||
},
|
||||
{ label: "capacity", content: "Ability" },
|
||||
{
|
||||
label: "channel",
|
||||
content: "To direct energy or resources toward a specific purpose",
|
||||
},
|
||||
{ label: "check", content: "To restrain, control, or reduce" },
|
||||
{ label: "coin", content: "To invent (e.g., coin a phrase)" },
|
||||
{ label: "compromise", content: "To endanger or make vulnerable" },
|
||||
{
|
||||
label: "discriminating",
|
||||
content: "Able to make fine distinctions (positive word)",
|
||||
},
|
||||
{ label: "doctor", content: "To tamper with" },
|
||||
{
|
||||
label: "economy / economical",
|
||||
content: "Thrift; using few words or resources (not about money)",
|
||||
},
|
||||
{
|
||||
label: "exploit",
|
||||
content: "To make use of (no negative connotation required)",
|
||||
},
|
||||
{ label: "facility", content: "Ease; ability to do something effortlessly" },
|
||||
{ label: "fluid", content: "Able to change shape; not rigid" },
|
||||
{ label: "foil", content: "To put a stop to; to thwart" },
|
||||
{ label: "harbor", content: "To hold or possess (e.g., harbor a belief)" },
|
||||
{
|
||||
label: "plastic",
|
||||
content: "Malleable; able to be changed (as in brain plasticity)",
|
||||
},
|
||||
{ label: "reserve / reservations", content: "To hold off; misgivings" },
|
||||
{ label: "sound", content: "Firm, stable, reliable, valid" },
|
||||
{ label: "static", content: "Unchanging; in a state of stasis" },
|
||||
{
|
||||
label: "table / shelve",
|
||||
content: "To reject or discard an idea or proposal",
|
||||
},
|
||||
{ label: "uniform", content: "Constant; unvarying" },
|
||||
{ label: "upset", content: "To interfere with an expected outcome" },
|
||||
];
|
||||
|
||||
const NEGATIVE_LOOKING = [
|
||||
{
|
||||
label: "critic / criticism",
|
||||
content:
|
||||
"A person who writes commentary, positive or negative; not inherently hostile",
|
||||
},
|
||||
{
|
||||
label: "discerning",
|
||||
content: "Perceptive; able to make fine distinctions",
|
||||
},
|
||||
{ label: "ineffable", content: "Beyond words; sublime; indescribable" },
|
||||
{ label: "infallible", content: "Unable to be wrong; perfectly reliable" },
|
||||
{ label: "ingenious", content: "Clever, brilliant" },
|
||||
{ label: "ingenuous", content: "Naive (note: distinct from ingenious)" },
|
||||
{ label: "inimitable", content: "Unique; one-of-a-kind" },
|
||||
{ label: "innocuous", content: "Harmless" },
|
||||
{ label: "invaluable", content: "Having immense value; priceless" },
|
||||
{ label: "unassuming", content: "Modest" },
|
||||
{ label: "unqualified", content: "Absolute (e.g., unqualified success)" },
|
||||
];
|
||||
|
||||
const EBRWWordsInContextLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
const [revealedVocab, setRevealedVocab] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleVocab = (key: string) => {
|
||||
setRevealedVocab((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
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());
|
||||
}, []);
|
||||
|
||||
useScrollReveal();
|
||||
|
||||
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-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="Sentence Completions"
|
||||
icon={BookOpen}
|
||||
/>
|
||||
<SectionMarker index={1} title="Context Signals" icon={Star} />
|
||||
<SectionMarker
|
||||
index={2}
|
||||
title="Meaning in Context"
|
||||
icon={Lightbulb}
|
||||
/>
|
||||
<SectionMarker index={3} title="Elimination Practice" icon={Target} />
|
||||
<SectionMarker index={4} title="Practice Questions" icon={Zap} />
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-4xl mx-auto">
|
||||
{/* ── SECTION 0: SENTENCE COMPLETIONS ── */}
|
||||
<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 w-fit">
|
||||
Craft & Structure — Domain 1
|
||||
</div>
|
||||
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
|
||||
Words in Context
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Identify the signal, determine the charge, predict your answer
|
||||
before reading options — context determines meaning, not the
|
||||
dictionary.
|
||||
</p>
|
||||
|
||||
{/* Two Question Subtypes */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
The Two Question Subtypes
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{[
|
||||
{
|
||||
num: "A",
|
||||
rule: "Sentence Completions",
|
||||
stem: '"Which choice completes the text with the most logical and precise word or phrase?"',
|
||||
desc: "A blank must be filled. Answer choices include challenging academic vocabulary (epitomize, succinct, anomaly). Requires both contextual reasoning AND knowledge of word meanings.",
|
||||
},
|
||||
{
|
||||
num: "B",
|
||||
rule: "Meaning in Context",
|
||||
stem: '"As used in the text, what does the word X most nearly mean?"',
|
||||
desc: "A familiar word is used in a non-standard definition. The dictionary meaning is VERY LIKELY to appear as a wrong answer — treat it as a red flag, not a confirmation.",
|
||||
},
|
||||
].map((r) => (
|
||||
<div
|
||||
key={r.num}
|
||||
className="bg-white border border-fuchsia-200 rounded-xl p-4 card-tilt"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="w-6 h-6 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{r.num}
|
||||
</span>
|
||||
<span className="font-bold text-fuchsia-900 text-sm">
|
||||
{r.rule}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 italic mb-2">{r.stem}</p>
|
||||
<p className="text-xs text-slate-600">{r.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* When to Study Vocabulary */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
When to Study Vocabulary
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
If you are already scoring above 90% on Words in Context
|
||||
questions, vocabulary study has diminishing returns. Focus on
|
||||
speed and elimination instead. If you are scoring below 70%,
|
||||
targeted vocabulary building — especially roots, prefixes, and the
|
||||
academic words below — is the single highest-ROI investment you
|
||||
can make for this question type.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-1">
|
||||
High Scorers (90%+)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Focus on elimination speed, second meanings, and advanced
|
||||
context clue patterns. Vocabulary drills are unlikely to gain
|
||||
points.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-1">
|
||||
Building Scorers (below 70%)
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Study the Academic Vocabulary and Common Second Meanings
|
||||
tables below. Learn 10 new words per week using flashcards
|
||||
with context sentences, not isolated definitions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4-Step Process */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-2">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
The 4-Step Process for Sentence Completions
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
title: "Read the entire passage",
|
||||
desc: "Identify key words or phrases. Consider the missing word in context of the full passage — focusing only on the blank line is a common error that leads to missing crucial information elsewhere.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Plug in your own word",
|
||||
desc: "If a word comes to mind, write it down. This reduces error by establishing what the blank requires before looking at options. Spend no more than a few seconds on this. If you are not certain what belongs, skip this step entirely — guessing a filler word when uncertain creates misdirection.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Play positive/negative",
|
||||
desc: "Determine whether the blank requires a positive, negative, or neutral word and write + or − on scratch paper. Then eliminate any answer choice that contradicts the charge. Go through options in order (A through D) to maintain systematic thinking and reduce careless error.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Plug in remaining options and compare",
|
||||
desc: "Choose the best fit. Focus on what a word means, not how it sounds — a word may seem odd in context yet be the precise match required.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.step}
|
||||
className="flex gap-3 bg-fuchsia-50 border border-fuchsia-100 rounded-xl p-4"
|
||||
>
|
||||
<span className="w-7 h-7 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-sm font-bold shrink-0">
|
||||
{s.step}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 text-sm">
|
||||
{s.title}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{s.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 1: CONTEXT SIGNALS ── */}
|
||||
<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">
|
||||
Context Signals
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Signal words tell you exactly what direction and charge the blank
|
||||
must have. Identify the signal before looking at any answer choice.
|
||||
</p>
|
||||
|
||||
{/* 3 Context Clue Signal Types */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
The 3 Context Clue Signal Types
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
type: "Type 1: Continuers",
|
||||
color: "bg-green-50 border-green-200",
|
||||
badge: "bg-green-600",
|
||||
words:
|
||||
"also, and, as well as, furthermore, in addition, just as, likewise, moreover, not only...but also, similarly",
|
||||
meaning:
|
||||
"The sentence continues in the same direction. The blank must have a similar meaning or charge to the surrounding context.",
|
||||
ex: 'Soil is poor and resources are ___. The continuer "and" links the blank to "poor" — the blank must also be negative, meaning something like scarce or limited.',
|
||||
},
|
||||
{
|
||||
type: "Type 2: Cause-and-Effect",
|
||||
color: "bg-blue-50 border-blue-200",
|
||||
badge: "bg-blue-600",
|
||||
words:
|
||||
"as a result, because, consequently, therefore, thus — and punctuation marks such as colons and dashes",
|
||||
meaning:
|
||||
"One idea causes or explains another. The blank must fit logically as either the cause or the effect of nearby information.",
|
||||
ex: 'Astronauts required mental evaluation because the ___ danger was as important as the physiological one. "Because" links the blank to mental evaluation — the blank must mean psychological.',
|
||||
},
|
||||
{
|
||||
type: "Type 3: Contradictors",
|
||||
color: "bg-red-50 border-red-200",
|
||||
badge: "bg-red-600",
|
||||
words:
|
||||
"although, but, despite, for all (= despite), however, in contrast, in spite of, meanwhile, nevertheless, on the other hand, though, unlike, whereas, while",
|
||||
meaning:
|
||||
"The sentence shifts direction. The blank must have the opposite meaning or charge from what precedes or follows the contradictor.",
|
||||
ex: 'The Afar Triangle seems quite ___. However, its appearance is deceptive, obscuring fiery lava just beneath. "However" signals contrast with fiery lava — the blank must mean calm or peaceful.',
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.type}
|
||||
className={`border rounded-xl p-4 ${s.color}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-white text-xs font-bold ${s.badge}`}
|
||||
>
|
||||
{s.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 italic mb-1">
|
||||
{s.words}
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 mb-2">{s.meaning}</p>
|
||||
<div className="bg-white/70 rounded-lg p-2">
|
||||
<p className="text-xs text-slate-600 italic">
|
||||
<span className="font-bold not-italic text-slate-700">
|
||||
Example:{" "}
|
||||
</span>
|
||||
{s.ex}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Patterns */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Advanced Sentence Structure Patterns
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-1">
|
||||
Parallel Structure
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
When a sentence uses constructions on either side of a paired
|
||||
word (both...and, not only...but also), the items must be
|
||||
grammatically parallel. This structure tells you what form the
|
||||
blank must take.
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 italic">
|
||||
<span className="font-bold not-italic text-slate-700">
|
||||
Example:{" "}
|
||||
</span>
|
||||
Praised for being both accessible and ___: while it captures
|
||||
clarity, it does so without sacrificing subtlety or
|
||||
complexity. Blank must parallel "accessible" — something like{" "}
|
||||
<em>nuanced</em> or <em>sophisticated</em>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-xl p-4">
|
||||
<p className="font-bold text-fuchsia-800 text-sm mb-1">
|
||||
Two Negatives Equal a Positive
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 mb-2">
|
||||
Watch for sentences in which the negation of a negative word
|
||||
produces a positive overall meaning. Students see the negative
|
||||
charge of individual words and miss the positive meaning they
|
||||
create together.
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 italic">
|
||||
<span className="font-bold not-italic text-slate-700">
|
||||
Example:{" "}
|
||||
</span>
|
||||
Other species play a less ___ role because their caterpillars
|
||||
consume harmful insects. The overall idea is positive
|
||||
(helpful). But "less + [something good] = negative." So the
|
||||
blank must be a negative word like <em>detrimental</em>, not a
|
||||
positive word like <em>beneficial</em>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roots & Prefixes */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal stagger-2">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Using Roots & Prefixes
|
||||
</h3>
|
||||
<p className="text-sm text-slate-700">
|
||||
Knowledge of Latin and Romance-language roots allows educated
|
||||
guesses about unfamiliar words — often more valuable than
|
||||
memorizing definitions because it applies to words never seen
|
||||
before.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{[
|
||||
["a-", "not (anomaly = unique)"],
|
||||
["de-", "not/away from (detrimental = harmful)"],
|
||||
["bene-", "good (beneficial = producing good)"],
|
||||
["facil-", "easy (facility = ease)"],
|
||||
].map(([prefix, meaning]) => (
|
||||
<div
|
||||
key={prefix}
|
||||
className="bg-white border border-fuchsia-100 rounded-lg px-3 py-2"
|
||||
>
|
||||
<p className="font-bold text-fuchsia-700 text-sm">{prefix}</p>
|
||||
<p className="text-xs text-slate-600">{meaning}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-xs text-amber-800 font-bold mb-1">
|
||||
Important Caveat
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
Some positive words have prefixes that ordinarily signal
|
||||
negatives (discerning, innate, invaluable). These are
|
||||
exceptions. In general, logical use of roots and prefixes
|
||||
produces reliable results.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-fuchsia-900 text-white rounded-2xl p-5 mb-8 scroll-reveal-scale golden-rule-glow">
|
||||
<p className="font-bold mb-1">Golden Rule — Sentence Completions</p>
|
||||
<p className="text-sm text-fuchsia-100">
|
||||
Read the entire passage, identify the signal word (continuer /
|
||||
cause-effect / contradictor), determine positive or negative
|
||||
charge, plug in your own word, then eliminate by charge before
|
||||
testing by meaning. Never look at answer choices first.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 2: MEANING IN CONTEXT ── */}
|
||||
<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-2">
|
||||
Meaning in Context
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Context determines meaning. A word means whatever the author uses it
|
||||
to mean in that passage — regardless of its dictionary definition.
|
||||
</p>
|
||||
|
||||
{/* Three Strategies */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Three Strategies for Meaning-in-Context Questions
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
num: 1,
|
||||
title: "Substitute Your Own Word",
|
||||
desc: "Use context to determine what word belongs, then find the answer choice that matches. This is the fastest approach for clear-context questions.",
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
title: "Plug In Each Answer Choice",
|
||||
desc: "Replace the word in question with each option and test whether it fits the logic of the passage. Eliminate choices that produce illogical or grammatically awkward sentences.",
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
title: "Play Positive/Negative, Then Plug In",
|
||||
desc: "Determine the charge of the blank, eliminate clearly opposite-charge answers, then test the remainder. Especially useful when meaning is less immediately clear.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.num}
|
||||
className="flex gap-3 bg-white border border-fuchsia-100 rounded-xl p-4"
|
||||
>
|
||||
<span className="w-7 h-7 rounded-full bg-fuchsia-600 text-white flex items-center justify-center text-sm font-bold shrink-0">
|
||||
{s.num}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800 text-sm">
|
||||
{s.title}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{s.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<p className="font-bold text-amber-900 text-sm mb-1">
|
||||
Secondary Meaning Indicator
|
||||
</p>
|
||||
<p className="text-xs text-slate-700">
|
||||
When all four answer choices are the same part of speech but one
|
||||
is a word normally used as a different part of speech (e.g.,{" "}
|
||||
<em>fluid</em> appearing as an adjective), this is a definitive
|
||||
signal that a second meaning is being tested. The
|
||||
unusual-looking option has an above-average chance of being
|
||||
correct and should be examined first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Second Meanings */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Common Second Meanings — Master Reference
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
These words frequently appear in Meaning-in-Context questions in
|
||||
their secondary definitions. Tap each card to reveal.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={SECOND_MEANINGS}
|
||||
columns={4}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Words That Look Negative But Are Not */}
|
||||
<div className="bg-fuchsia-50 border border-fuchsia-200 rounded-2xl p-6 mb-8 space-y-4 scroll-reveal stagger-1">
|
||||
<h3 className="text-lg font-bold text-fuchsia-900">
|
||||
Words That Look Negative But Are Not
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
These words trap students who eliminate them based on their prefix
|
||||
or appearance. Tap each to see the actual definition.
|
||||
</p>
|
||||
<RevealCardGrid
|
||||
cards={NEGATIVE_LOOKING}
|
||||
columns={3}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Academic Vocabulary — Interactive */}
|
||||
<div className="rounded-2xl p-6 mb-8 bg-white border border-slate-200 space-y-4 scroll-reveal stagger-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Academic Vocabulary to Know
|
||||
</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-400">
|
||||
{revealedVocab.size}/{ALL_VOCAB_KEYS.length} revealed
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRevealedVocab(
|
||||
revealedVocab.size >= ALL_VOCAB_KEYS.length
|
||||
? new Set()
|
||||
: new Set(ALL_VOCAB_KEYS),
|
||||
)
|
||||
}
|
||||
className="text-xs font-bold text-fuchsia-600 hover:text-fuchsia-800 transition-colors"
|
||||
>
|
||||
{revealedVocab.size >= ALL_VOCAB_KEYS.length
|
||||
? "Hide All"
|
||||
: "Reveal All"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
Click each card to test yourself, then tap to reveal the answer.
|
||||
</p>
|
||||
|
||||
<p className="font-bold text-fuchsia-800 text-sm">
|
||||
Key Claim / Argument Verbs
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{CLAIM_VERBS.map((v) => {
|
||||
const isRevealed = revealedVocab.has(v.label);
|
||||
return (
|
||||
<button
|
||||
key={v.label}
|
||||
onClick={() => toggleVocab(v.label)}
|
||||
className={`text-left rounded-xl p-3 border transition-all duration-200 ${isRevealed ? "bg-fuchsia-50 border-fuchsia-300 shadow-sm" : "bg-slate-50 border-slate-200 hover:border-fuchsia-300 hover:shadow-sm"}`}
|
||||
>
|
||||
<p className="font-bold text-fuchsia-700 text-xs mb-1">
|
||||
{v.label}
|
||||
</p>
|
||||
{isRevealed ? (
|
||||
<p className="text-xs text-slate-600 font-mono leading-relaxed">
|
||||
{v.words}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400 italic">
|
||||
tap to reveal
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="font-bold text-fuchsia-800 text-sm mt-2">
|
||||
Key Science & Academic Terms
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||
{SCIENCE_TERMS.map((t) => {
|
||||
const isRevealed = revealedVocab.has(t.word);
|
||||
return (
|
||||
<button
|
||||
key={t.word}
|
||||
onClick={() => toggleVocab(t.word)}
|
||||
className={`text-left rounded-xl p-3 border transition-all duration-200 ${isRevealed ? "bg-fuchsia-50 border-fuchsia-300 shadow-sm" : "bg-slate-50 border-slate-200 hover:border-fuchsia-300 hover:shadow-sm"}`}
|
||||
>
|
||||
<p className="font-bold text-fuchsia-700 text-xs mb-1">
|
||||
{t.word}
|
||||
</p>
|
||||
{isRevealed ? (
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
{t.def}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400 italic">
|
||||
tap to reveal
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-fuchsia-900 text-white rounded-2xl p-5 mb-8 scroll-reveal-scale golden-rule-glow">
|
||||
<p className="font-bold mb-1">Golden Rule — Meaning in Context</p>
|
||||
<p className="text-sm text-fuchsia-100">
|
||||
Never rely on the dictionary definition. Context determines
|
||||
meaning. The standard definition will very commonly appear as a
|
||||
wrong answer — treat its presence as a red flag, not a
|
||||
confirmation.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 3: WIDGET ── */}
|
||||
<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-2">
|
||||
Elimination Practice
|
||||
</h2>
|
||||
<p className="text-lg text-slate-500 mb-8">
|
||||
Use the signal + your prediction to eliminate wrong answers one by
|
||||
one.
|
||||
</p>
|
||||
<ContextEliminationWidget
|
||||
exercises={VOCAB_EXERCISES}
|
||||
accentColor="fuchsia"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ── SECTION 4: PRACTICE ── */}
|
||||
<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">
|
||||
Practice Questions
|
||||
</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-fuchsia-900 text-white font-bold rounded-full hover:bg-fuchsia-700 transition-colors"
|
||||
>
|
||||
Finish Lesson ✓
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EBRWWordsInContextLesson;
|
||||
@ -102,7 +102,7 @@ const LinesAnglesLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* Section 1: Parallel Lines */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
|
||||
@ -87,7 +87,7 @@ const SystemsEquationsLesson: React.FC<LessonProps> = ({ onFinish }) => {
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
|
||||
<div className="flex-1 lg:ml-64 md:p-12 max-w-full mx-auto">
|
||||
{/* Section 1: Number of Solutions */}
|
||||
<section
|
||||
ref={(el) => {
|
||||
|
||||
@ -68,6 +68,8 @@ export interface LessonMetadata {
|
||||
| "Triangle"
|
||||
| "Square"
|
||||
| "Chart"
|
||||
| "GitMerge"
|
||||
| "ArrowRightLeft"
|
||||
| "Target"
|
||||
| "Calculator"
|
||||
| "TrendingUp"
|
||||
|
||||
@ -1,156 +1,102 @@
|
||||
import type { LessonMetadata, QuizData } from "../types/lesson";
|
||||
|
||||
export const EBRW_LESSONS: LessonMetadata[] = [
|
||||
// --- INFORMATION AND IDEAS ---
|
||||
// --- CRAFT & STRUCTURE ---
|
||||
{
|
||||
id: "ebrw-main-idea",
|
||||
title: "Main Idea Across Text Types",
|
||||
id: "ebrw-words-in-context",
|
||||
title: "Words in Context",
|
||||
description:
|
||||
"Identifying the author's central claim or purpose in nonfiction, literary fiction, drama, and poetry passages.",
|
||||
iconName: "Target",
|
||||
color: "teal",
|
||||
category: "Information and Ideas",
|
||||
"Determine the meaning of a word or phrase as it is used in a passage by analyzing context, tone, and logical flow.",
|
||||
iconName: "BookOpen",
|
||||
color: "fuchsia",
|
||||
category: "Craft & Structure",
|
||||
},
|
||||
{
|
||||
id: "ebrw-explicit-meaning",
|
||||
title: "Explicit Meaning & Key Details",
|
||||
id: "ebrw-text-structure-purpose",
|
||||
title: "Text Structure & Purpose",
|
||||
description:
|
||||
"Locating directly stated information in nonfiction, literary fiction, and poetry — no inference required.",
|
||||
iconName: "BookOpen",
|
||||
"Analyze how authors organize passages and why specific sentences, quotes, or examples are included.",
|
||||
iconName: "Layers",
|
||||
color: "fuchsia",
|
||||
category: "Craft & Structure",
|
||||
},
|
||||
{
|
||||
id: "ebrw-cross-text-connections",
|
||||
title: "Cross-Text Connections",
|
||||
description:
|
||||
"Compare two short texts and determine how their ideas relate, agree, or differ.",
|
||||
iconName: "GitMerge",
|
||||
color: "fuchsia",
|
||||
category: "Craft & Structure",
|
||||
},
|
||||
|
||||
// --- INFORMATION & IDEAS ---
|
||||
{
|
||||
id: "ebrw-central-ideas-details",
|
||||
title: "Central Ideas & Details",
|
||||
description:
|
||||
"Identify the central claim or key supporting ideas in nonfiction, fiction, drama, or poetry passages.",
|
||||
iconName: "Target",
|
||||
color: "teal",
|
||||
category: "Information and Ideas",
|
||||
category: "Information & Ideas",
|
||||
},
|
||||
{
|
||||
id: "ebrw-inferences",
|
||||
title: "Inferences & Text Completion",
|
||||
title: "Inferences",
|
||||
description:
|
||||
"Drawing logical conclusions and selecting sentences that most logically complete a passage's argument.",
|
||||
"Draw logical conclusions based on textual evidence, even when the answer is not directly stated.",
|
||||
iconName: "BookOpen",
|
||||
color: "teal",
|
||||
category: "Information and Ideas",
|
||||
category: "Information & Ideas",
|
||||
},
|
||||
{
|
||||
id: "ebrw-graphic-displays",
|
||||
title: "Reading Graphic Displays",
|
||||
id: "ebrw-command-of-evidence",
|
||||
title: "Command of Evidence",
|
||||
description:
|
||||
"Accurately reading bar graphs, line graphs, and tables — identifying trends, comparing data, and evaluating claims.",
|
||||
"Identify which evidence best supports a claim or conclusion made in a passage.",
|
||||
iconName: "BarChart3",
|
||||
color: "teal",
|
||||
category: "Information and Ideas",
|
||||
},
|
||||
|
||||
// --- CRAFT AND STRUCTURE ---
|
||||
{
|
||||
id: "ebrw-craft-structure",
|
||||
title: "Craft & Structure: Author's Purpose",
|
||||
description:
|
||||
"Analyzing why an author includes specific details, quotes, or examples — understanding structural choices and rhetorical evaluation.",
|
||||
iconName: "Layers",
|
||||
color: "fuchsia",
|
||||
category: "Craft and Structure",
|
||||
},
|
||||
{
|
||||
id: "ebrw-vocab-precise",
|
||||
title: "Vocabulary: Logical & Precise Word Choice",
|
||||
description:
|
||||
"Selecting the most precise or logically appropriate word to complete a sentence — matching logic, tone, and strength.",
|
||||
iconName: "Calculator",
|
||||
color: "fuchsia",
|
||||
category: "Craft and Structure",
|
||||
},
|
||||
{
|
||||
id: "ebrw-vocab-meaning",
|
||||
title: "Vocabulary: Most Nearly Means",
|
||||
description:
|
||||
"Interpreting words and phrases as used in context — not their dictionary definition. Context always overrides the textbook.",
|
||||
iconName: "BookOpen",
|
||||
color: "fuchsia",
|
||||
category: "Craft and Structure",
|
||||
},
|
||||
|
||||
// --- EXPRESSION OF IDEAS ---
|
||||
{
|
||||
id: "ebrw-expression-ideas",
|
||||
title: "Expression of Ideas: Synthesis",
|
||||
description:
|
||||
"Revising and combining information from notes or two texts, and incorporating evidence to support a specific claim.",
|
||||
iconName: "TrendingUp",
|
||||
color: "rose",
|
||||
category: "Expression of Ideas",
|
||||
},
|
||||
{
|
||||
id: "ebrw-transitions",
|
||||
title: "Transitions: Conjunctive Adverbs",
|
||||
description:
|
||||
"Choosing the right logical connector (however, therefore, furthermore) to improve the effectiveness of written expression.",
|
||||
iconName: "Grid",
|
||||
color: "rose",
|
||||
category: "Expression of Ideas",
|
||||
category: "Information & Ideas",
|
||||
},
|
||||
|
||||
// --- STANDARD ENGLISH CONVENTIONS ---
|
||||
{
|
||||
id: "ebrw-commas",
|
||||
title: "Punctuation: Commas",
|
||||
id: "ebrw-boundaries",
|
||||
title: "Boundaries",
|
||||
description:
|
||||
"Comma rules for FANBOYS, introductory elements, nonessential information, and lists — the most-tested punctuation on the SAT.",
|
||||
"Use punctuation correctly to separate clauses and sentences, including commas, semicolons, and colons.",
|
||||
iconName: "BookOpen",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
category: "Standard English",
|
||||
},
|
||||
{
|
||||
id: "ebrw-semicolons-colons",
|
||||
title: "Punctuation: Semicolons & Colons",
|
||||
id: "ebrw-form-structure-sense",
|
||||
title: "Form, Structure & Sense",
|
||||
description:
|
||||
"Using semicolons to join independent clauses and colons to introduce lists or explanations after a complete sentence.",
|
||||
iconName: "BookOpen",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
},
|
||||
{
|
||||
id: "ebrw-dashes-apostrophes",
|
||||
title: "Punctuation: Dashes, Apostrophes & More",
|
||||
description:
|
||||
"Em dashes for emphasis, apostrophes for possession and contractions, end marks, and avoiding unnecessary punctuation.",
|
||||
iconName: "BookOpen",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
},
|
||||
{
|
||||
id: "ebrw-subject-verb",
|
||||
title: "Agreement: Subject–Verb",
|
||||
description:
|
||||
"Finding the true subject and matching the verb — not misled by prepositional phrases, interrupting clauses, or inverted order.",
|
||||
"Ensure sentences are grammatically correct and logically structured, including agreement, modifiers, and verb form.",
|
||||
iconName: "Scale",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
category: "Standard English",
|
||||
},
|
||||
|
||||
// --- EXPRESSION OF IDEAS ---
|
||||
{
|
||||
id: "ebrw-transitions",
|
||||
title: "Transitions",
|
||||
description:
|
||||
"Choose the transition word or phrase that best connects ideas and improves the flow of a passage.",
|
||||
iconName: "ArrowRightLeft",
|
||||
color: "rose",
|
||||
category: "Expression of Ideas",
|
||||
},
|
||||
{
|
||||
id: "ebrw-pronouns",
|
||||
title: "Agreement: Pronouns & Confused Words",
|
||||
id: "ebrw-rhetorical-synthesis",
|
||||
title: "Rhetorical Synthesis",
|
||||
description:
|
||||
"Pronoun-antecedent agreement, who vs. whom, and frequently confused pairs: its/it's, their/there/they're, whose/who's.",
|
||||
iconName: "BookOpen",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
},
|
||||
{
|
||||
id: "ebrw-verbs",
|
||||
title: "Parts of Speech: Verbs & Verb Forms",
|
||||
description:
|
||||
"Tense consistency, past perfect, present perfect, finite vs. nonfinite verbs, irregular forms, and subjunctive mood.",
|
||||
iconName: "BookOpen",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
},
|
||||
{
|
||||
id: "ebrw-sentence-structure",
|
||||
title: "Sentence Structure: Parallelism & Modifiers",
|
||||
description:
|
||||
"Parallel construction in series and comparisons, and fixing misplaced or dangling modifiers — two of the SAT's top traps.",
|
||||
iconName: "Layers",
|
||||
color: "purple",
|
||||
category: "Standard English Conventions",
|
||||
"Combine information from notes or sources to support a claim or improve the effectiveness of writing.",
|
||||
iconName: "TrendingUp",
|
||||
color: "rose",
|
||||
category: "Expression of Ideas",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user