diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..bb2d7d8f --- /dev/null +++ b/TODO.md @@ -0,0 +1,26 @@ +# TODO: Fix ESLint Issues + +## Step 1: Fix scripts/sitemap-generator.js + +- Replace unused 'e' in catch block with underscore. + +## Step 2: Fix simulations/test.jsx - Remove unused imports + +- Remove 'useMemo' from React imports. +- Remove 'getTimeScale' from Time.js imports. +- Remove 'PHYSICS_CONTROLS' and 'VISUALIZATION_CONTROLS' from config imports. + +## Step 3: Fix simulations/test.jsx - Fix useEffect dependencies + +- Add 'inputs.timeScale' to the first useEffect dependency array. +- Extract 'simData["Total Energy"]' to a variable for the warnings useEffect. +- Add 'simData' to the warnings useEffect dependency array. + +## Step 4: Fix simulations/test.jsx - Refactor warnings logic + +- Move warnings calculation from useEffect to useMemo to avoid setState in effect. +- Update the component to use warnings from useMemo. + +## Step 5: Run ESLint to verify fixes + +- Execute `npm run lint` to check if all issues are resolved. diff --git a/app/(core)/components/CollapsibleSection.tsx b/app/(core)/components/CollapsibleSection.tsx new file mode 100644 index 00000000..4dec77a1 --- /dev/null +++ b/app/(core)/components/CollapsibleSection.tsx @@ -0,0 +1,45 @@ +// CollapsibleSection.tsx +"use client"; +import React, { useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; + +interface CollapsibleSectionProps { + title: string; + children: React.ReactNode; + defaultExpanded?: boolean; + icon?: React.ReactNode; + className?: string; +} + +export default function CollapsibleSection({ + title, + children, + defaultExpanded = false, + icon, + className = "", +}: CollapsibleSectionProps) { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + + return ( +
+ + {isExpanded &&
{children}
} +
+ ); +} diff --git a/app/(core)/components/EducationalTheorySection.tsx b/app/(core)/components/EducationalTheorySection.tsx new file mode 100644 index 00000000..618a29ae --- /dev/null +++ b/app/(core)/components/EducationalTheorySection.tsx @@ -0,0 +1,63 @@ +// EducationalTheorySection.tsx +"use client"; +import React from "react"; +import LearningObjectives from "./LearningObjectives.tsx"; +import PhysicsEquations from "./PhysicsEquations.tsx"; +import PhysicsWarnings from "./PhysicsWarnings.tsx"; +import GuidedExperiments from "./GuidedExperiments.tsx"; + +interface EducationalTheorySectionProps { + learningObjectives: { + title: string; + goals: string[]; + variables: string[]; + }; + physicsEquations: Array<{ + name: string; + formula: string; + description?: string; + }>; + warnings: Array<{ + id: string; + message: string; + severity: "warning" | "error"; + }>; + guidedExperiments: Array<{ + id: string; + name: string; + description: string; + instructions: string[]; + question?: string; + parameters: Record; + }>; + onApplyExperiment: ( + params: Record + ) => void; +} + +export default function EducationalTheorySection({ + learningObjectives, + physicsEquations, + warnings, + guidedExperiments, + onApplyExperiment, +}: EducationalTheorySectionProps) { + return ( +
+ + + + + {warnings.length > 0 && } + + +
+ ); +} diff --git a/app/(core)/components/GuidedExperiments.tsx b/app/(core)/components/GuidedExperiments.tsx new file mode 100644 index 00000000..81b1120f --- /dev/null +++ b/app/(core)/components/GuidedExperiments.tsx @@ -0,0 +1,76 @@ +// GuidedExperiments.tsx +"use client"; +import React, { useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCheck } from "@fortawesome/free-solid-svg-icons"; + +interface Experiment { + id: string; + name: string; + description: string; + instructions: string[]; + question?: string; + parameters: Record; +} + +interface GuidedExperimentsProps { + experiments: Experiment[]; + onApplyExperiment: ( + params: Record + ) => void; +} + +export default function GuidedExperiments({ + experiments, + onApplyExperiment, +}: GuidedExperimentsProps) { + const [activeExperiment, setActiveExperiment] = useState(null); + + const handleApply = (experiment: Experiment) => { + onApplyExperiment(experiment.parameters); + setActiveExperiment(experiment.id); + }; + + return ( +
+
+ {experiments.map((experiment) => ( +
+
+

{experiment.name}

+ +
+

{experiment.description}

+
+ Instructions: +
    + {experiment.instructions.map((instruction, i) => ( +
  1. + {instruction} +
  2. + ))} +
+
+ {experiment.question && ( +
+ Question: {experiment.question} +
+ )} +
+ ))} +
+
+ ); +} diff --git a/app/(core)/components/Layout.jsx b/app/(core)/components/Layout.jsx index 737afe31..5cf0cf07 100644 --- a/app/(core)/components/Layout.jsx +++ b/app/(core)/components/Layout.jsx @@ -18,8 +18,8 @@ export default function Layout({ )} {showGradient && } diff --git a/app/(core)/components/LearningObjectives.tsx b/app/(core)/components/LearningObjectives.tsx new file mode 100644 index 00000000..1d573bc4 --- /dev/null +++ b/app/(core)/components/LearningObjectives.tsx @@ -0,0 +1,35 @@ +// LearningObjectives.tsx +"use client"; +import React from "react"; + +interface LearningObjectivesProps { + goals: string[]; + variables: string[]; +} + +export default function LearningObjectives({ + goals, + variables, +}: LearningObjectivesProps) { + return ( +
+
+

Learning Goals

+
    + {goals.map((goal, i) => ( +
  • {goal}
  • + ))} +
+
+ +
+

Key Variables

+
    + {variables.map((variable, i) => ( +
  • {variable}
  • + ))} +
+
+
+ ); +} diff --git a/app/(core)/components/PhysicsEquations.tsx b/app/(core)/components/PhysicsEquations.tsx new file mode 100644 index 00000000..917ed48d --- /dev/null +++ b/app/(core)/components/PhysicsEquations.tsx @@ -0,0 +1,28 @@ +// PhysicsEquations.tsx +"use client"; +import React from "react"; + +interface PhysicsEquationsProps { + equations: Array<{ + name: string; + formula: string; + description?: string; + }>; +} + +export default function PhysicsEquations({ equations }: PhysicsEquationsProps) { + return ( +
+
+ {equations.map((eq, i) => ( +
+
{eq.formula}
+ {eq.description && ( +
{eq.description}
+ )} +
+ ))} +
+
+ ); +} diff --git a/app/(core)/components/PhysicsWarnings.tsx b/app/(core)/components/PhysicsWarnings.tsx new file mode 100644 index 00000000..4c34dca2 --- /dev/null +++ b/app/(core)/components/PhysicsWarnings.tsx @@ -0,0 +1,33 @@ +// PhysicsWarnings.tsx +"use client"; +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; + +interface Warning { + id: string; + message: string; + severity: "warning" | "error"; +} + +interface PhysicsWarningsProps { + warnings: Warning[]; +} + +export default function PhysicsWarnings({ warnings }: PhysicsWarningsProps) { + if (warnings.length === 0) return null; + + return ( +
+ {warnings.map((warning) => ( +
+ + {warning.message} +
+ ))} +
+ ); +} diff --git a/app/(core)/components/SimulationLayout.jsx b/app/(core)/components/SimulationLayout.jsx index fc9b8473..acccdcbe 100644 --- a/app/(core)/components/SimulationLayout.jsx +++ b/app/(core)/components/SimulationLayout.jsx @@ -16,6 +16,7 @@ export default function SimulationLayout({ onLoad, children, dynamicInputs, + hideDefaultControls = false, }) { const theory = useMemo(() => { const chapter = chapters.find((ch) => ch.link === simulation); @@ -42,13 +43,15 @@ export default function SimulationLayout({ {/* 1. Render the Canvas */} {children} - {/* 2. Render the Main Controls */} - + {/* 2. Render the Main Controls (unless hidden) */} + {!hideDefaultControls && ( + + )} {/* 3. Render the Dynamic Inputs */} {dynamicInputs} diff --git a/app/(core)/components/inputs/DynamicInputs.jsx b/app/(core)/components/inputs/DynamicInputs.jsx index e87bd821..947bd3dd 100644 --- a/app/(core)/components/inputs/DynamicInputs.jsx +++ b/app/(core)/components/inputs/DynamicInputs.jsx @@ -3,66 +3,124 @@ import CheckboxInput from "./CheckboxInput.jsx"; import ColorInput from "./ColorInput.jsx"; import SelectInput from "./SelectInput.jsx"; -export default function DynamicInputs({ config, values, onChange }) { - return ( -
- {config.map((field) => { - const commonProps = { - name: field.name, - label: field.label, - }; +function renderInput(field, values, onChange) { + // Check if field should be conditionally rendered + if (field.showIf) { + const condition = field.showIf; + const shouldShow = condition.every(({ key, value }) => { + if (value === true) return values[key] === true; + if (value === false) return values[key] === false; + return values[key] === value; + }); + if (!shouldShow) return null; + } + + const commonProps = { + name: field.name, + label: field.label, + }; - if (field.type === "number") { - return ( - onChange(field.name, Number(e.target.value))} - /> - ); - } + if (field.type === "number") { + return ( + onChange(field.name, Number(e.target.value))} + /> + ); + } - if (field.type === "checkbox") { - return ( - onChange(field.name, e.target.checked)} - /> - ); - } + if (field.type === "checkbox") { + return ( + onChange(field.name, e.target.checked)} + /> + ); + } - if (field.type === "color") { - return ( - onChange(field.name, e.target.value)} - /> - ); - } + if (field.type === "color") { + return ( + onChange(field.name, e.target.value)} + /> + ); + } + + if (field.type === "select") { + return ( + onChange(field.name, e.target.value)} + /> + ); + } + return null; +} - if (field.type === "select") { - return ( - onChange(field.name, e.target.value)} - /> - ); - } - return null; - })} +export default function DynamicInputs({ + config, + values, + onChange, + grouped = false, +}) { + // If grouped, separate by category + if (grouped) { + const physicsControls = config.filter((f) => f.category === "physics"); + const visualizationControls = config.filter( + (f) => f.category === "visualization" + ); + const otherControls = config.filter((f) => !f.category); + + return ( + <> + {physicsControls.length > 0 && ( +
+

Physics Controls

+
+ {physicsControls.map((field) => + renderInput(field, values, onChange) + )} +
+
+ )} + {visualizationControls.length > 0 && ( +
+

Visualization Controls

+
+ {visualizationControls.map((field) => + renderInput(field, values, onChange) + )} +
+
+ )} + {otherControls.length > 0 && ( +
+ {otherControls.map((field) => renderInput(field, values, onChange))} +
+ )} + + ); + } + + // Default: render all together + return ( +
+ {config.map((field) => renderInput(field, values, onChange))}
); } diff --git a/app/(core)/components/inputs/SelectInput.jsx b/app/(core)/components/inputs/SelectInput.jsx index a645fa81..6fddbae3 100644 --- a/app/(core)/components/inputs/SelectInput.jsx +++ b/app/(core)/components/inputs/SelectInput.jsx @@ -3,20 +3,37 @@ import React from "react"; import PropTypes from "prop-types"; function SelectInput({ label, name, options, value, onChange, placeholder }) { + const handleChange = (e) => { + const selectedValue = e.target.value; + // Convert to number if the value is numeric (for gravity and similar numeric selects) + const numericValue = + !isNaN(selectedValue) && + selectedValue !== "" && + !isNaN(parseFloat(selectedValue)) + ? parseFloat(selectedValue) + : selectedValue; + // Create a synthetic event object that matches what onChange expects + const syntheticEvent = { + target: { + name, + value: numericValue, + }, + }; + onChange(syntheticEvent); + }; + return ( -
- {label && ( - - )} +
+