fix(ui): change ui theme color
feat(calc): add geogebra based graph calculator for tests
This commit is contained in:
250
src/components/Calculator.tsx
Normal file
250
src/components/Calculator.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
import { useEffect, useRef, useState, useCallback } 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 };
|
||||
Reference in New Issue
Block a user