feat(lessons): add lessons from client db
This commit is contained in:
173
src/components/lessons/InequalityRegionWidget.tsx
Normal file
173
src/components/lessons/InequalityRegionWidget.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
const InequalityRegionWidget: React.FC = () => {
|
||||
// State for Inequalities: y > or < mx + b
|
||||
// isGreater: true for >=, false for <=
|
||||
const [ineq1, setIneq1] = useState({ m: 1, b: 1, isGreater: true });
|
||||
const [ineq2, setIneq2] = useState({ m: -0.5, b: -2, isGreater: false });
|
||||
|
||||
const [testPoint, setTestPoint] = useState({ x: 0, y: 0 });
|
||||
const isDragging = useRef(false);
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
// Viewport
|
||||
const range = 10;
|
||||
const size = 300;
|
||||
const scale = size / (range * 2);
|
||||
const center = size / 2;
|
||||
|
||||
// Helpers
|
||||
const toPx = (val: number, isY = false) => {
|
||||
return isY ? center - val * scale : center + val * scale;
|
||||
};
|
||||
|
||||
const fromPx = (px: number, isY = false) => {
|
||||
return isY ? (center - px) / scale : (px - center) / scale;
|
||||
};
|
||||
|
||||
// Generate polygon points for shading
|
||||
const getRegionPoints = (m: number, b: number, isGreater: boolean) => {
|
||||
const xMin = -range;
|
||||
const xMax = range;
|
||||
const yAtMin = m * xMin + b;
|
||||
const yAtMax = m * xMax + b;
|
||||
|
||||
// y limit is the top (range) or bottom (-range) of the graph
|
||||
const limitY = isGreater ? range : -range;
|
||||
|
||||
const p1 = { x: xMin, y: yAtMin };
|
||||
const p2 = { x: xMax, y: yAtMax };
|
||||
const p3 = { x: xMax, y: limitY };
|
||||
const p4 = { x: xMin, y: limitY };
|
||||
|
||||
return `${toPx(p1.x)},${toPx(p1.y, true)} ${toPx(p2.x)},${toPx(p2.y, true)} ${toPx(p3.x)},${toPx(p3.y, true)} ${toPx(p4.x)},${toPx(p4.y, true)}`;
|
||||
};
|
||||
|
||||
const getLinePath = (m: number, b: number) => {
|
||||
const x1 = -range;
|
||||
const y1 = m * x1 + b;
|
||||
const x2 = range;
|
||||
const y2 = m * x2 + b;
|
||||
return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
|
||||
};
|
||||
|
||||
// Interaction
|
||||
const handleInteraction = (e: React.MouseEvent) => {
|
||||
if (!svgRef.current) return;
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const x = fromPx(e.clientX - rect.left);
|
||||
const y = fromPx(e.clientY - rect.top, true);
|
||||
// Clamp
|
||||
const cx = Math.max(-range, Math.min(range, x));
|
||||
const cy = Math.max(-range, Math.min(range, y));
|
||||
setTestPoint({ x: cx, y: cy });
|
||||
};
|
||||
|
||||
// Logic Check
|
||||
const check1 = ineq1.isGreater ? testPoint.y >= ineq1.m * testPoint.x + ineq1.b : testPoint.y <= ineq1.m * testPoint.x + ineq1.b;
|
||||
const check2 = ineq2.isGreater ? testPoint.y >= ineq2.m * testPoint.x + ineq2.b : testPoint.y <= ineq2.m * testPoint.x + ineq2.b;
|
||||
const isSolution = check1 && check2;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
|
||||
{/* Controls */}
|
||||
<div className="w-full md:w-1/3 space-y-6">
|
||||
{/* Inequality 1 */}
|
||||
<div className={`p-4 rounded-lg border ${check1 ? 'bg-indigo-50 border-indigo-200' : 'bg-slate-50 border-slate-200'}`}>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-bold text-indigo-800 text-sm">Region 1 (Blue)</span>
|
||||
<button
|
||||
onClick={() => setIneq1(p => ({...p, isGreater: !p.isGreater}))}
|
||||
className="text-xs bg-white border border-indigo-200 px-2 py-1 rounded font-bold text-indigo-600"
|
||||
>
|
||||
{ineq1.isGreater ? 'y ≥ ...' : 'y ≤ ...'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500"><span>Slope</span><span>{ineq1.m}</span></div>
|
||||
<input type="range" min="-4" max="4" step="0.5" value={ineq1.m} onChange={e => setIneq1({...ineq1, m: parseFloat(e.target.value)})} className="w-full h-1 bg-indigo-200 rounded accent-indigo-600"/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500"><span>Y-Int</span><span>{ineq1.b}</span></div>
|
||||
<input type="range" min="-8" max="8" step="1" value={ineq1.b} onChange={e => setIneq1({...ineq1, b: parseFloat(e.target.value)})} className="w-full h-1 bg-indigo-200 rounded accent-indigo-600"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inequality 2 */}
|
||||
<div className={`p-4 rounded-lg border ${check2 ? 'bg-rose-50 border-rose-200' : 'bg-slate-50 border-slate-200'}`}>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-bold text-rose-800 text-sm">Region 2 (Red)</span>
|
||||
<button
|
||||
onClick={() => setIneq2(p => ({...p, isGreater: !p.isGreater}))}
|
||||
className="text-xs bg-white border border-rose-200 px-2 py-1 rounded font-bold text-rose-600"
|
||||
>
|
||||
{ineq2.isGreater ? 'y ≥ ...' : 'y ≤ ...'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500"><span>Slope</span><span>{ineq2.m}</span></div>
|
||||
<input type="range" min="-4" max="4" step="0.5" value={ineq2.m} onChange={e => setIneq2({...ineq2, m: parseFloat(e.target.value)})} className="w-full h-1 bg-rose-200 rounded accent-rose-600"/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500"><span>Y-Int</span><span>{ineq2.b}</span></div>
|
||||
<input type="range" min="-8" max="8" step="1" value={ineq2.b} onChange={e => setIneq2({...ineq2, b: parseFloat(e.target.value)})} className="w-full h-1 bg-rose-200 rounded accent-rose-600"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`p-3 rounded-lg text-center font-bold text-sm border-2 transition-colors ${isSolution ? 'bg-emerald-100 border-emerald-400 text-emerald-800' : 'bg-slate-100 border-slate-300 text-slate-500'}`}>
|
||||
Test Point: ({testPoint.x.toFixed(1)}, {testPoint.y.toFixed(1)}) <br/>
|
||||
{isSolution ? "SOLUTION FOUND" : "NOT A SOLUTION"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Graph */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
<div className="relative w-[300px] h-[300px] bg-white border border-slate-200 rounded-xl overflow-hidden cursor-crosshair">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="300" height="300" viewBox="0 0 300 300"
|
||||
onMouseDown={(e) => { isDragging.current = true; handleInteraction(e); }}
|
||||
onMouseMove={(e) => { if(isDragging.current) handleInteraction(e); }}
|
||||
onMouseUp={() => isDragging.current = false}
|
||||
onMouseLeave={() => isDragging.current = false}
|
||||
>
|
||||
<defs>
|
||||
<pattern id="grid-ineq" width="15" height="15" patternUnits="userSpaceOnUse">
|
||||
<path d="M 15 0 L 0 0 0 15" fill="none" stroke="#f8fafc" strokeWidth="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid-ineq)" />
|
||||
|
||||
{/* Axes */}
|
||||
<line x1="0" y1={center} x2={size} y2={center} stroke="#cbd5e1" strokeWidth="2" />
|
||||
<line x1={center} y1="0" x2={center} y2={size} stroke="#cbd5e1" strokeWidth="2" />
|
||||
|
||||
{/* Region 1 */}
|
||||
<polygon points={getRegionPoints(ineq1.m, ineq1.b, ineq1.isGreater)} fill="rgba(99, 102, 241, 0.2)" />
|
||||
<path d={getLinePath(ineq1.m, ineq1.b)} stroke="#4f46e5" strokeWidth="2" />
|
||||
|
||||
{/* Region 2 */}
|
||||
<polygon points={getRegionPoints(ineq2.m, ineq2.b, ineq2.isGreater)} fill="rgba(225, 29, 72, 0.2)" />
|
||||
<path d={getLinePath(ineq2.m, ineq2.b)} stroke="#e11d48" strokeWidth="2" />
|
||||
|
||||
{/* Test Point */}
|
||||
<circle
|
||||
cx={toPx(testPoint.x)} cy={toPx(testPoint.y, true)} r="6"
|
||||
fill={isSolution ? "#10b981" : "#64748b"} stroke="white" strokeWidth="2" className="shadow-sm"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InequalityRegionWidget;
|
||||
Reference in New Issue
Block a user