Files
edbridge-scholars/src/components/Calculator.tsx
2026-03-12 02:39:34 +06:00

251 lines
8.0 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { X, Calculator, Maximize2, Minimize2 } from "lucide-react";
// ─── GeoGebra type shim ───────────────────────────────────────────────────────
declare global {
interface Window {
GGBApplet: new (
params: Record<string, unknown>,
defer?: boolean,
) => {
inject: (containerId: string) => void;
};
ggbApplet?: {
reset: () => void;
setXML: (xml: string) => void;
};
}
}
// ─── Hook: load GeoGebra script once ─────────────────────────────────────────
const GEOGEBRA_SCRIPT = "https://www.geogebra.org/apps/deployggb.js";
const useGeoGebraScript = () => {
const [ready, setReady] = useState(false);
useEffect(() => {
if (document.querySelector(`script[src="${GEOGEBRA_SCRIPT}"]`)) {
if (window.GGBApplet) setReady(true);
return;
}
const script = document.createElement("script");
script.src = GEOGEBRA_SCRIPT;
script.async = true;
script.onload = () => setReady(true);
document.head.appendChild(script);
}, []);
return ready;
};
// ─── GeoGebra Calculator ──────────────────────────────────────────────────────
const GeoGebraCalculator = ({ containerId }: { containerId: string }) => {
const scriptReady = useGeoGebraScript();
const injected = useRef(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
// Measure the wrapper first — GeoGebra needs explicit px dimensions
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
setDims({ w: Math.floor(width), h: Math.floor(height) });
}
}
});
ro.observe(el);
return () => ro.disconnect();
}, []);
useEffect(() => {
if (!scriptReady || !dims || injected.current) return;
injected.current = true;
const params = {
appName: "graphing",
width: dims.w,
height: dims.h,
showToolBar: true,
showAlgebraInput: true,
showMenuBar: false,
enableLabelDrags: true,
enableShiftDragZoom: true,
enableRightClick: true,
showZoomButtons: true,
capturingThreshold: null,
showFullscreenButton: false,
scale: 1,
disableAutoScale: false,
allowUpscale: false,
clickToLoad: false,
appletOnLoad: () => {},
useBrowserForJS: false,
showLogging: false,
errorDialogsActive: true,
showTutorialLink: false,
showSuggestionButtons: false,
language: "en",
id: "ggbApplet",
};
try {
const applet = new window.GGBApplet(params, true);
applet.inject(containerId);
} catch (e) {
console.error("GeoGebra init error:", e);
}
}, [scriptReady, dims, containerId]);
return (
<div ref={wrapperRef} className="w-full h-full">
{!dims && (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-sm font-satoshi gap-2">
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
Loading calculator...
</div>
)}
<div id={containerId} style={{ width: dims?.w, height: dims?.h }} />
</div>
);
};
// ─── Modal ────────────────────────────────────────────────────────────────────
interface GraphCalculatorModalProps {
open: boolean;
onClose: () => void;
}
const GraphCalculatorModal = ({ open, onClose }: GraphCalculatorModalProps) => {
const [fullscreen, setFullscreen] = useState(false);
const containerId = "geogebra-container";
// Trap focus & keyboard dismiss
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
// Prevent body scroll while open
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [open]);
if (!open) return null;
return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="dialog"
aria-modal="true"
aria-label="Graph Calculator"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={onClose}
/>
{/* Panel */}
<div
className={`
relative z-10 flex flex-col bg-white rounded-2xl shadow-2xl overflow-hidden
transition-all duration-300
${
fullscreen
? "w-screen h-screen rounded-none"
: "w-[95vw] h-[90vh] max-w-5xl"
}
`}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 bg-white shrink-0">
<div className="flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-purple-50 border border-purple-100">
<Calculator size={16} className="text-purple-500" />
</div>
<span className="font-satoshi-bold text-gray-800 text-sm">
Graph Calculator
</span>
<span className="text-[10px] font-satoshi text-gray-400 bg-gray-100 px-2 py-0.5 rounded-full">
Powered by GeoGebra
</span>
</div>
<div className="flex items-center gap-1.5">
<button
onClick={() => setFullscreen((f) => !f)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition"
title={fullscreen ? "Exit fullscreen" : "Fullscreen"}
>
{fullscreen ? <Minimize2 size={15} /> : <Maximize2 size={15} />}
</button>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 hover:text-red-500 hover:bg-red-50 transition"
title="Close"
>
<X size={16} />
</button>
</div>
</div>
{/* GeoGebra canvas area */}
<div className="flex-1 overflow-hidden">
<GeoGebraCalculator containerId={containerId} />
</div>
</div>
</div>,
document.body,
);
};
// ─── Trigger button + modal — drop this wherever you need it ──────────────────
export const GraphCalculatorButton = () => {
const [open, setOpen] = useState(false);
return (
<>
<button
onClick={() => setOpen(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-full font-satoshi-medium text-sm"
>
<Calculator size={16} />
Calculator
</button>
<GraphCalculatorModal open={open} onClose={() => setOpen(false)} />
</>
);
};
// ─── Standalone modal export if you need to control it externally ─────────────
export { GraphCalculatorModal };