feat(lessons): add lessons from client db
This commit is contained in:
186
src/components/lessons/InteractiveTransversal.tsx
Normal file
186
src/components/lessons/InteractiveTransversal.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type Relationship = 'none' | 'vertical' | 'linear' | 'corresponding' | 'alt-interior' | 'same-side';
|
||||
|
||||
const InteractiveTransversal: React.FC = () => {
|
||||
const [activeRel, setActiveRel] = useState<Relationship>('same-side');
|
||||
|
||||
// SVG Config
|
||||
const width = 500;
|
||||
const height = 300;
|
||||
|
||||
const line1Y = 100;
|
||||
const line2Y = 200;
|
||||
|
||||
// Transversal passes through (200, 100) and (300, 200)
|
||||
// Slope is 1 (45 degrees down-right)
|
||||
const intersection1 = { x: 200, y: 100 };
|
||||
const intersection2 = { x: 300, y: 200 };
|
||||
|
||||
// Angle Definitions (SVG y-down coordinates)
|
||||
// 0 deg = Right, 90 deg = Down, 180 deg = Left, 270 deg = Up
|
||||
// Transversal vector is (1, 1), angle is 45 deg.
|
||||
// Opposite ray is 225 deg.
|
||||
|
||||
const angles = [
|
||||
// Intersection 1 (Top)
|
||||
{ id: 1, cx: intersection1.x, cy: intersection1.y, start: 180, end: 225, labelPos: 202.5, quadrant: 'TL' }, // Top-Left (Acute)
|
||||
{ id: 2, cx: intersection1.x, cy: intersection1.y, start: 225, end: 360, labelPos: 292.5, quadrant: 'TR' }, // Top-Right (Obtuse)
|
||||
{ id: 3, cx: intersection1.x, cy: intersection1.y, start: 0, end: 45, labelPos: 22.5, quadrant: 'BR' }, // Bottom-Right (Acute)
|
||||
{ id: 4, cx: intersection1.x, cy: intersection1.y, start: 45, end: 180, labelPos: 112.5, quadrant: 'BL' }, // Bottom-Left (Obtuse)
|
||||
|
||||
// Intersection 2 (Bottom)
|
||||
{ id: 5, cx: intersection2.x, cy: intersection2.y, start: 180, end: 225, labelPos: 202.5, quadrant: 'TL' },
|
||||
{ id: 6, cx: intersection2.x, cy: intersection2.y, start: 225, end: 360, labelPos: 292.5, quadrant: 'TR' },
|
||||
{ id: 7, cx: intersection2.x, cy: intersection2.y, start: 0, end: 45, labelPos: 22.5, quadrant: 'BR' },
|
||||
{ id: 8, cx: intersection2.x, cy: intersection2.y, start: 45, end: 180, labelPos: 112.5, quadrant: 'BL' },
|
||||
];
|
||||
|
||||
const getArcPath = (cx: number, cy: number, r: number, startDeg: number, endDeg: number) => {
|
||||
// Convert to radians
|
||||
const startRad = (startDeg * Math.PI) / 180;
|
||||
const endRad = (endDeg * Math.PI) / 180;
|
||||
|
||||
const x1 = cx + r * Math.cos(startRad);
|
||||
const y1 = cy + r * Math.sin(startRad);
|
||||
const x2 = cx + r * Math.cos(endRad);
|
||||
const y2 = cy + r * Math.sin(endRad);
|
||||
|
||||
const largeArc = (endDeg - startDeg) > 180 ? 1 : 0;
|
||||
|
||||
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
|
||||
};
|
||||
|
||||
const getLabelPos = (cx: number, cy: number, r: number, angleDeg: number) => {
|
||||
const rad = (angleDeg * Math.PI) / 180;
|
||||
return {
|
||||
x: cx + r * Math.cos(rad),
|
||||
y: cy + r * Math.sin(rad)
|
||||
};
|
||||
};
|
||||
|
||||
const getStyles = (id: number) => {
|
||||
const base = { fill: 'transparent', stroke: 'transparent', label: 'text-slate-400' };
|
||||
const highlightBlue = { fill: 'rgba(99, 102, 241, 0.3)', stroke: '#4f46e5', label: 'text-indigo-600 font-bold' };
|
||||
const highlightPink = { fill: 'rgba(244, 63, 94, 0.3)', stroke: '#e11d48', label: 'text-rose-600 font-bold' };
|
||||
|
||||
switch (activeRel) {
|
||||
case 'vertical':
|
||||
// 1 & 3 are equal
|
||||
if ([1, 3].includes(id)) return highlightBlue;
|
||||
return base;
|
||||
case 'linear':
|
||||
// 1 & 2 are supplementary
|
||||
if (id === 1) return highlightBlue;
|
||||
if (id === 2) return highlightPink;
|
||||
return base;
|
||||
case 'corresponding':
|
||||
// 2 & 6 are equal
|
||||
if ([2, 6].includes(id)) return highlightBlue;
|
||||
return base;
|
||||
case 'alt-interior':
|
||||
// 4 & 6 are equal
|
||||
if ([4, 6].includes(id)) return highlightBlue;
|
||||
return base;
|
||||
case 'same-side':
|
||||
// 3 & 6 are supplementary
|
||||
if (id === 3) return highlightBlue;
|
||||
if (id === 6) return highlightPink;
|
||||
return base;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
switch (activeRel) {
|
||||
case 'vertical': return "Vertical Angles are equal (e.g. ∠1 = ∠3)";
|
||||
case 'linear': return "Linear Pairs sum to 180° (e.g. ∠1 + ∠2 = 180°)";
|
||||
case 'corresponding': return "Corresponding Angles are equal (e.g. ∠2 = ∠6)";
|
||||
case 'alt-interior': return "Alternate Interior Angles are equal (e.g. ∠4 = ∠6)";
|
||||
case 'same-side': return "Same-Side Interior sum to 180° (e.g. ∠3 + ∠6 = 180°)";
|
||||
default: return "Select a relationship to highlight";
|
||||
}
|
||||
};
|
||||
|
||||
const buttons: { id: Relationship, label: string }[] = [
|
||||
{ id: 'vertical', label: 'Vertical Angles' },
|
||||
{ id: 'linear', label: 'Linear Pair' },
|
||||
{ id: 'corresponding', label: 'Corresponding' },
|
||||
{ id: 'alt-interior', label: 'Alt. Interior' },
|
||||
{ id: 'same-side', label: 'Same-Side Interior' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center bg-white p-8 rounded-xl shadow-sm border border-slate-200">
|
||||
<div className="flex flex-wrap gap-2 justify-center mb-8">
|
||||
{buttons.map(btn => (
|
||||
<button
|
||||
key={btn.id}
|
||||
onClick={() => setActiveRel(activeRel === btn.id ? 'none' : btn.id)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-bold transition-all border ${
|
||||
activeRel === btn.id
|
||||
? 'bg-slate-900 text-white border-slate-900 shadow-md'
|
||||
: 'bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full flex justify-center">
|
||||
<div className="absolute top-0 left-0 w-full text-center">
|
||||
<p className="text-slate-500 font-medium">{getDescription()}</p>
|
||||
</div>
|
||||
|
||||
<svg width={width} height={height} className="mt-8 select-none">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
|
||||
<path d="M0,0 L0,8 L12,4 z" fill="#64748b" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* Parallel Lines */}
|
||||
<line x1="50" y1={line1Y} x2="450" y2={line1Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
<line x1="450" y1={line1Y} x2="50" y2={line1Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
|
||||
<line x1="50" y1={line2Y} x2="450" y2={line2Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
<line x1="450" y1={line2Y} x2="50" y2={line2Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
|
||||
|
||||
{/* Transversal (infinite line simulation) */}
|
||||
<line x1="100" y1="0" x2="400" y2="300" stroke="#0f172a" strokeWidth="3" strokeLinecap="round" />
|
||||
|
||||
{/* Angles */}
|
||||
{angles.map((angle) => {
|
||||
const styles = getStyles(angle.id);
|
||||
const r = 35;
|
||||
const labelPos = getLabelPos(angle.cx, angle.cy, r + 15, angle.labelPos);
|
||||
|
||||
return (
|
||||
<g key={angle.id}>
|
||||
<path
|
||||
d={getArcPath(angle.cx, angle.cy, r, angle.start, angle.end)}
|
||||
fill={styles.fill}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={styles.stroke === 'transparent' ? 0 : 2}
|
||||
/>
|
||||
<text
|
||||
x={labelPos.x}
|
||||
y={labelPos.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className={`text-sm select-none ${styles.label}`}
|
||||
>
|
||||
{angle.id}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractiveTransversal;
|
||||
Reference in New Issue
Block a user