feat(lessons): add lessons from client db

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

View File

@ -0,0 +1,364 @@
import React, { useState, useRef } from 'react';
type Mode = 'chords' | 'secants';
interface Point {
x: number;
y: number;
}
const PowerOfPointWidget: React.FC = () => {
const [mode, setMode] = useState<Mode>('chords');
// -- Common State --
const svgRef = useRef<SVGSVGElement>(null);
const isDragging = useRef<string | null>(null);
const center = { x: 200, y: 180 };
const radius = 100;
// -- Chords Mode State --
// Store angles for points A, B, C, D on the circle
const [chordAngles, setChordAngles] = useState({
a: 220, b: 40, // Chord 1
c: 140, d: 320 // Chord 2
});
// -- Secants Mode State --
// P is external point.
// Secant 1 defined by angle theta1 (offset from center-P line)
// Secant 2 defined by angle theta2
const [secantState, setSecantState] = useState({
px: 380, py: 180, // Point P
theta1: 15, // Angle offset for secant 1
});
// --- Helper Math ---
const getPosOnCircle = (deg: number) => ({
x: center.x + radius * Math.cos(deg * Math.PI / 180),
y: center.y + radius * Math.sin(deg * Math.PI / 180)
});
const dist = (p1: Point, p2: Point) => Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
const getIntersection = (p1: Point, p2: Point, p3: Point, p4: Point) => {
// Line AB represented as a1x + b1y = c1
const a1 = p2.y - p1.y;
const b1 = p1.x - p2.x;
const c1 = a1 * p1.x + b1 * p1.y;
// Line CD represented as a2x + b2y = c2
const a2 = p4.y - p3.y;
const b2 = p3.x - p4.x;
const c2 = a2 * p3.x + b2 * p3.y;
const determinant = a1 * b2 - a2 * b1;
if (Math.abs(determinant) < 0.001) return null; // Parallel
const x = (b2 * c1 - b1 * c2) / determinant;
const y = (a1 * c2 - a2 * c1) / determinant;
// Check if inside circle
if (dist({x,y}, center) > radius + 1) return null;
return { x, y };
};
// --- Interaction Handlers ---
const handleChordDrag = (e: React.MouseEvent, key: string) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const dx = e.clientX - rect.left - center.x;
const dy = e.clientY - rect.top - center.y;
let deg = Math.atan2(dy, dx) * 180 / Math.PI;
if (deg < 0) deg += 360;
setChordAngles(prev => ({ ...prev, [key]: deg }));
};
const handleSecantDrag = (e: React.MouseEvent) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (isDragging.current === 'P') {
// Constrain P outside
const dx = x - center.x;
const dy = y - center.y;
const d = Math.sqrt(dx*dx + dy*dy);
if (d > radius + 20) {
setSecantState(prev => ({...prev, px: x, py: y}));
} else {
const ang = Math.atan2(dy, dx);
setSecantState(prev => ({
...prev,
px: center.x + (radius+20)*Math.cos(ang),
py: center.y + (radius+20)*Math.sin(ang)
}));
}
} else if (isDragging.current === 'SecantEnd') {
// Calculate angle relative to PO line
// Vector PO
const pdx = center.x - secantState.px;
const pdy = center.y - secantState.py;
const poAngle = Math.atan2(pdy, pdx);
// Vector PA (mouse to P)
const mdx = x - secantState.px;
const mdy = y - secantState.py;
const mAngle = Math.atan2(mdy, mdx);
let diff = (mAngle - poAngle) * 180 / Math.PI;
// Normalize to -180 to 180
while (diff > 180) diff -= 360;
while (diff < -180) diff += 360;
// Clamp to hit circle. Max angle is asin(R/dist)
const distPO = Math.sqrt(pdx*pdx + pdy*pdy);
const maxAngle = Math.asin(radius/distPO) * 180 / Math.PI;
// Clamp
const clamped = Math.max(-maxAngle + 1, Math.min(maxAngle - 1, diff));
setSecantState(prev => ({...prev, theta1: clamped}));
}
};
// --- Render Helpers ---
const renderChords = () => {
const A = getPosOnCircle(chordAngles.a);
const B = getPosOnCircle(chordAngles.b);
const C = getPosOnCircle(chordAngles.c);
const D = getPosOnCircle(chordAngles.d);
const E = getIntersection(A, B, C, D);
const valid = !!E;
const ae = valid ? dist(A, E) : 0;
const eb = valid ? dist(E, B) : 0;
const ce = valid ? dist(C, E) : 0;
const ed = valid ? dist(E, D) : 0;
const points = [
{ k: 'a', p: A, l: 'A', c: '#7c3aed' },
{ k: 'b', p: B, l: 'B', c: '#7c3aed' },
{ k: 'c', p: C, l: 'C', c: '#059669' },
{ k: 'd', p: D, l: 'D', c: '#059669' }
];
return (
<>
<line x1={A.x} y1={A.y} x2={B.x} y2={B.y} stroke="#7c3aed" strokeWidth="3" />
<line x1={C.x} y1={C.y} x2={D.x} y2={D.y} stroke="#059669" strokeWidth="3" />
{/* Points */}
{points.map((pt) => (
<g key={pt.k} onMouseDown={() => isDragging.current = pt.k} className="cursor-pointer hover:scale-110 transition-transform">
<circle cx={pt.p.x} cy={pt.p.y} r="15" fill="transparent" />
<circle cx={pt.p.x} cy={pt.p.y} r="6" fill={pt.c} stroke="white" strokeWidth="2" />
<text x={pt.p.x} y={pt.p.y - 12} textAnchor="middle" className="text-sm font-bold fill-slate-700">{pt.l}</text>
</g>
))}
{valid && (
<>
<circle cx={E.x} cy={E.y} r="4" fill="#0f172a" />
<text x={E.x + 10} y={E.y} className="text-xs font-bold fill-slate-500">E</text>
</>
)}
{/* Info Panel */}
<div className="absolute top-4 left-4 bg-white/90 p-4 rounded-xl border border-slate-200 shadow-sm backdrop-blur-sm pointer-events-none select-none">
{!valid ? (
<p className="text-red-500 font-bold">Chords must intersect inside!</p>
) : (
<div className="space-y-3 font-mono text-sm">
<div className="flex gap-4">
<div>
<div className="text-xs font-bold text-violet-600">Purple Chord</div>
<div>{ae.toFixed(0)} × {eb.toFixed(0)} = <strong>{(ae*eb).toFixed(0)}</strong></div>
</div>
</div>
<div className="flex gap-4">
<div>
<div className="text-xs font-bold text-emerald-600">Green Chord</div>
<div>{ce.toFixed(0)} × {ed.toFixed(0)} = <strong>{(ce*ed).toFixed(0)}</strong></div>
</div>
</div>
<div className="h-px bg-slate-200"></div>
<p className="text-slate-500 text-xs text-center font-sans">
AE · EB = CE · ED
</p>
</div>
)}
</div>
</>
);
};
const renderSecant = () => {
const { px, py, theta1 } = secantState;
// Calculate Tangent Point T (Upper)
const dx = px - center.x;
const dy = py - center.y;
const distPO = Math.sqrt(dx*dx + dy*dy);
const anglePO = Math.atan2(dy, dx);
const angleOffset = Math.acos(radius/distPO);
const tAngle = anglePO - angleOffset;
const T = {
x: center.x + radius * Math.cos(tAngle),
y: center.y + radius * Math.sin(tAngle)
};
const tangentLen = Math.sqrt(distPO*distPO - radius*radius);
// Calculate Secant Intersection Points
// Secant Line angle
const secantAngle = anglePO + theta1 * Math.PI / 180;
const vx = px - center.x;
const vy = py - center.y;
const cos = Math.cos(secantAngle);
const sin = Math.sin(secantAngle);
// t^2 + 2(V.D)t + (V^2 - R^2) = 0
const b = 2 * (vx * cos + vy * sin);
const c = vx*vx + vy*vy - radius*radius;
const det = b*b - 4*c;
let A = {x:0, y:0}, B = {x:0, y:0};
let valid = false;
if (det > 0) {
const tFar = (-b - Math.sqrt(det)) / 2;
const tNear = (-b + Math.sqrt(det)) / 2;
// A is Near (External part)
A = { x: px + tNear * cos, y: py + tNear * sin };
// B is Far (Whole secant endpoint)
B = { x: px + tFar * cos, y: py + tFar * sin };
valid = true;
}
const distPA = valid ? dist({x:px, y:py}, A) : 0;
const distPB = valid ? dist({x:px, y:py}, B) : 0;
return (
<>
{/* Tangent Line */}
<line x1={px} y1={py} x2={T.x} y2={T.y} stroke="#e11d48" strokeWidth="3" />
<circle cx={T.x} cy={T.y} r="5" fill="#e11d48" />
<text x={T.x} y={T.y - 10} className="text-xs font-bold fill-rose-600">T</text>
{/* Secant Line (Draw full segment P to B) */}
{valid && <line x1={px} y1={py} x2={B.x} y2={B.y} stroke="#7c3aed" strokeWidth="3" />}
{valid && (
<>
{/* Point A (Near/External) */}
<circle cx={A.x} cy={A.y} r="5" fill="#7c3aed" />
<text x={A.x + 15} y={A.y} className="text-xs font-bold fill-violet-600">A</text>
{/* Point B (Far/Whole) */}
<circle cx={B.x} cy={B.y} r="5" fill="#7c3aed" />
<text x={B.x - 15} y={B.y} className="text-xs font-bold fill-violet-600">B</text>
</>
)}
{/* Point P */}
<g onMouseDown={() => isDragging.current = 'P'} className="cursor-grab active:cursor-grabbing">
<circle cx={px} cy={py} r="15" fill="transparent" />
<circle cx={px} cy={py} r="6" fill="#0f172a" stroke="white" strokeWidth="2" />
<text x={px + 10} y={py} className="text-sm font-bold fill-slate-800">P</text>
</g>
{/* Drag Handle for Secant Angle (at B, the far end) */}
{valid && (
<circle
cx={B.x} cy={B.y} r="12" fill="transparent" stroke="white" strokeWidth="2" strokeDasharray="2,2"
className="cursor-pointer hover:stroke-violet-400"
onMouseDown={() => isDragging.current = 'SecantEnd'}
/>
)}
{/* Info */}
<div className="absolute top-4 left-4 bg-white/90 p-4 rounded-xl border border-slate-200 shadow-sm backdrop-blur-sm pointer-events-none select-none">
<div className="space-y-3 font-mono text-sm">
<div className="mb-2">
<div className="text-xs font-bold text-rose-600 uppercase">Tangent² (PT²)</div>
<div>{tangentLen.toFixed(0)}² = <strong>{(tangentLen*tangentLen).toFixed(0)}</strong></div>
</div>
<div>
<div className="text-xs font-bold text-violet-600 uppercase">Secant (PA · PB)</div>
<div className="flex items-center gap-1">
<span title="External Part (PA)">{distPA.toFixed(0)}</span>
<span className="text-slate-400">×</span>
<span title="Whole Secant (PB)">{distPB.toFixed(0)}</span>
<span className="text-slate-400">=</span>
<strong>{(distPA*distPB).toFixed(0)}</strong>
</div>
</div>
<div className="h-px bg-slate-200 my-2"></div>
<p className="text-slate-500 text-xs text-center font-sans">
Tangent² = External × Whole
</p>
</div>
</div>
</>
);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current) return;
if (mode === 'chords') {
// Check if dragging specific points
if (['a','b','c','d'].includes(isDragging.current as string)) {
handleChordDrag(e, isDragging.current as string);
}
} else {
handleSecantDrag(e);
}
};
return (
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
<div className="flex gap-4 mb-6 justify-center">
<button
onClick={() => setMode('chords')}
className={`px-4 py-2 rounded-full font-bold text-sm transition-all ${mode === 'chords' ? 'bg-slate-900 text-white shadow-md' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Intersecting Chords
</button>
<button
onClick={() => setMode('secants')}
className={`px-4 py-2 rounded-full font-bold text-sm transition-all ${mode === 'secants' ? 'bg-slate-900 text-white shadow-md' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Tangent-Secant
</button>
</div>
<div className="relative flex justify-center bg-slate-50 rounded-xl border border-slate-100 overflow-hidden">
<svg
ref={svgRef}
width="500" height="360"
className="select-none"
onMouseMove={handleMouseMove}
onMouseUp={() => isDragging.current = null}
onMouseLeave={() => isDragging.current = null}
>
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e2e8f0" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
{/* Circle */}
<circle cx={center.x} cy={center.y} r={radius} fill="white" stroke="#94a3b8" strokeWidth="2" />
<circle cx={center.x} cy={center.y} r="3" fill="#cbd5e1" />
{mode === 'chords' ? renderChords() : renderSecant()}
</svg>
</div>
<div className="mt-4 text-center text-sm text-slate-500">
{mode === 'chords'
? "Drag the colored points along the circle."
: "Drag point P or the secant endpoint B."
}
</div>
</div>
);
};
export default PowerOfPointWidget;