From c28c35d8fd67520b354babb0369c8693ed19f0d9 Mon Sep 17 00:00:00 2001 From: Yonatan Hattav Date: Tue, 26 Nov 2024 19:57:05 +0100 Subject: [PATCH] Saving scenarios (#24) * basic working saveing * wrap in context replacing useSettings --- src/App.tsx | 105 +++++++------- .../GravitySimulator/GravitySimulator.tsx | 46 ++++-- .../SaveScenarioModal/SaveScenarioModal.tsx | 92 ++++++++++++ .../ScenarioPanel/ScenarioPanel.tsx | 65 ++++++++- .../SimulatorSettings/SimulatorSettings.tsx | 2 +- src/contexts/SettingsContext.tsx | 132 ++++++++++++++++++ src/hooks/useSettings.ts | 65 --------- 7 files changed, 373 insertions(+), 134 deletions(-) create mode 100644 src/components/SaveScenarioModal/SaveScenarioModal.tsx create mode 100644 src/contexts/SettingsContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 52debd2..cab4765 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { GravitySection } from "./sections/GravitySection"; import { DebugInfo } from "./components/DebugInfo"; import { DebugData } from "./types/Debug"; import "./App.css"; +import { SettingsProvider } from "./contexts/SettingsContext"; const { Content, Header } = Layout; @@ -41,59 +42,61 @@ function App() { }, []); return ( - -
-
-

Gravity Simulator

-
- - - - - - - - - - - - + + +
+
+

Gravity Simulator

+
-
-
- - - - - - {debugData && } - + + + + + + + {debugData && } + + -
+ ); } diff --git a/src/components/GravitySimulator/GravitySimulator.tsx b/src/components/GravitySimulator/GravitySimulator.tsx index 626b8de..1a1fef0 100644 --- a/src/components/GravitySimulator/GravitySimulator.tsx +++ b/src/components/GravitySimulator/GravitySimulator.tsx @@ -14,7 +14,7 @@ import { import { getContainerOffset } from "../../utils/dom/domUtils"; import { INITIAL_GRAVITY_POINTS } from "../../constants/physics"; import { SimulatorSettings } from "../SimulatorSettings/SimulatorSettings"; -import { useSettings } from "../../hooks/useSettings"; +import { useSettings } from "../../contexts/SettingsContext"; import { throttle } from "lodash"; import "../../styles/global.scss"; import { MdFullscreen, MdFullscreenExit } from "react-icons/md"; @@ -27,6 +27,7 @@ import { VscLibrary } from "react-icons/vsc"; import { ScenarioPanel } from "../ScenarioPanel/ScenarioPanel"; import { Scenario } from "../../types/scenario"; import { SettingOutlined } from "@ant-design/icons"; +import { SaveScenarioModal } from "../SaveScenarioModal/SaveScenarioModal"; interface ParticleMechanics { position: Point2D; @@ -81,12 +82,17 @@ export const GravitySimulator: React.FC = ({ ); const [isDragging, setIsDragging] = useState(false); const [isDraggingNewStar, setIsDraggingNewStar] = useState(false); - const { settings: physicsConfig, updateSettings } = useSettings(); + const { + settings: physicsConfig, + updateSettings, + saveScenario, + } = useSettings(); const [isFullscreen, setIsFullscreen] = useState(false); const [throttledPointerPos, setThrottledPointerPos] = useState(pointerPos); const [isPaused, setIsPaused] = useState(false); const [isScenarioPanelOpen, setIsScenarioPanelOpen] = useState(false); const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false); + const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); const toggleFullscreen = useCallback(() => { if (!document.fullscreenElement) { @@ -342,17 +348,29 @@ export const GravitySimulator: React.FC = ({ }); }, []); - const exportScenario = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - const scenario: SimulationScenario = { - settings: physicsConfig, - gravityPoints, - particles: particles.map(({ trails, force, ...particle }) => particle), + const exportScenario = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsSaveModalOpen(true); + }, []); + + const handleSaveScenario = useCallback( + (name: string) => { + const scenario: Scenario = { + id: Math.random().toString(36).substr(2, 9), + name, + description: "User saved scenario", + data: { + settings: physicsConfig, + gravityPoints, + particles: particles.map( + ({ trails, force, ...particle }) => particle + ), + }, }; - console.log(JSON.stringify(scenario, null, 2)); + saveScenario(scenario); + setIsSaveModalOpen(false); }, - [physicsConfig, gravityPoints, particles] + [physicsConfig, gravityPoints, particles, saveScenario] ); const handleSelectScenario = useCallback( @@ -602,6 +620,12 @@ export const GravitySimulator: React.FC = ({ onSelectScenario={handleSelectScenario} /> + setIsSaveModalOpen(false)} + onSave={handleSaveScenario} + /> + void; + onSave: (name: string) => void; +} + +export const SaveScenarioModal: React.FC = ({ + isOpen, + onClose, + onSave, +}) => { + const [scenarioName, setScenarioName] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (scenarioName.trim()) { + onSave(scenarioName); + setScenarioName(""); + } + }; + + return ( + + {isOpen && ( + e.stopPropagation()} + className="floating-panel" + style={{ + position: "absolute", + top: "70px", + right: "20px", + width: "280px", + padding: "20px", + color: "rgba(255, 255, 255, 0.9)", + }} + > +

+ Save Scenario +

+
+ setScenarioName(e.target.value)} + placeholder="Enter scenario name" + style={{ + width: "100%", + padding: "8px 12px", + background: "rgba(255, 255, 255, 0.1)", + border: "1px solid rgba(255, 255, 255, 0.2)", + borderRadius: "4px", + color: "white", + marginBottom: "15px", + }} + /> +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/ScenarioPanel/ScenarioPanel.tsx b/src/components/ScenarioPanel/ScenarioPanel.tsx index c72c12b..b711b88 100644 --- a/src/components/ScenarioPanel/ScenarioPanel.tsx +++ b/src/components/ScenarioPanel/ScenarioPanel.tsx @@ -5,7 +5,7 @@ import { Scenario } from "../../types/scenario"; import { defaultScenarios } from "../../scenarios/defaults"; import "./ScenarioPanel.scss"; import { VscLibrary } from "react-icons/vsc"; - +import { useSettings } from "../../contexts/SettingsContext"; interface ScenarioPanelProps { onSelectScenario: (scenario: Scenario) => void; isOpen: boolean; @@ -18,6 +18,7 @@ export const ScenarioPanel: React.FC = ({ onClose, }) => { const [activeTab, setActiveTab] = useState("1"); + const { savedScenarios, deleteSavedScenario } = useSettings(); return ( @@ -127,11 +128,63 @@ export const ScenarioPanel: React.FC = ({ ), children: ( -
- Coming soon... +
+ {savedScenarios.length === 0 ? ( +
+ No saved scenarios yet +
+ ) : ( + savedScenarios.map((scenario) => ( + onSelectScenario(scenario)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+

+ {scenario.name} +

+ +
+

+ {scenario.description} +

+
+ )) + )}
), }, diff --git a/src/components/SimulatorSettings/SimulatorSettings.tsx b/src/components/SimulatorSettings/SimulatorSettings.tsx index 876f1ea..3b04fda 100644 --- a/src/components/SimulatorSettings/SimulatorSettings.tsx +++ b/src/components/SimulatorSettings/SimulatorSettings.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { PHYSICS_CONFIG, SETTINGS_METADATA } from "../../constants/physics"; -import { useSettings } from "../../hooks/useSettings"; +import { useSettings } from "../../contexts/SettingsContext"; interface SimulatorSettingsProps { onSettingsChange: (settings: typeof PHYSICS_CONFIG) => void; diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx new file mode 100644 index 0000000..0979371 --- /dev/null +++ b/src/contexts/SettingsContext.tsx @@ -0,0 +1,132 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; +import { PHYSICS_CONFIG } from "../constants/physics"; +import { Scenario } from "../types/scenario"; + +const STORAGE_KEYS = { + SETTINGS: "simulatorSettings", + SHOW_DEV: "showDevSettings", + SAVED_SCENARIOS: "savedScenarios", +} as const; + +interface SettingsContextType { + settings: typeof PHYSICS_CONFIG; + showDevSettings: boolean; + savedScenarios: Scenario[]; + updateSettings: (newSettings: Partial) => void; + updateShowDevSettings: (show: boolean) => void; + resetSettings: () => void; + saveScenario: (scenario: Scenario) => void; + deleteSavedScenario: (scenarioId: string) => void; + isDevelopment: boolean; +} + +const SettingsContext = createContext(null); + +export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [settings, setSettings] = useState(() => { + const savedSettings = localStorage.getItem(STORAGE_KEYS.SETTINGS); + if (savedSettings) { + const parsedSettings = JSON.parse(savedSettings); + const validSettings = Object.keys(parsedSettings).reduce((acc, key) => { + if (key in PHYSICS_CONFIG) { + acc[key] = parsedSettings[key]; + } + return acc; + }, {} as Partial); + + return { ...PHYSICS_CONFIG, ...validSettings }; + } + return PHYSICS_CONFIG; + }); + + const [showDevSettings, setShowDevSettings] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEYS.SHOW_DEV); + return saved ? JSON.parse(saved) : false; + }); + + const [savedScenarios, setSavedScenarios] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEYS.SAVED_SCENARIOS); + return saved ? JSON.parse(saved) : []; + }); + + const updateSettings = useCallback( + (newSettings: Partial) => { + const validNewSettings = Object.keys(newSettings).reduce((acc, key) => { + if (key in PHYSICS_CONFIG) { + acc[key] = newSettings[key as keyof typeof PHYSICS_CONFIG]; + } + return acc; + }, {} as Partial); + + setSettings((prevSettings) => { + const updatedSettings = { ...prevSettings, ...validNewSettings }; + localStorage.setItem( + STORAGE_KEYS.SETTINGS, + JSON.stringify(updatedSettings) + ); + return updatedSettings; + }); + }, + [] + ); + + const updateShowDevSettings = useCallback((show: boolean) => { + setShowDevSettings(show); + localStorage.setItem(STORAGE_KEYS.SHOW_DEV, JSON.stringify(show)); + }, []); + + const resetSettings = useCallback(() => { + setSettings(PHYSICS_CONFIG); + localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(PHYSICS_CONFIG)); + }, []); + + const saveScenario = useCallback((scenario: Scenario) => { + setSavedScenarios((prevScenarios) => { + const updatedScenarios = [...prevScenarios, scenario]; + localStorage.setItem( + STORAGE_KEYS.SAVED_SCENARIOS, + JSON.stringify(updatedScenarios) + ); + return updatedScenarios; + }); + }, []); + + const deleteSavedScenario = useCallback((scenarioId: string) => { + setSavedScenarios((prevScenarios) => { + const updatedScenarios = prevScenarios.filter((s) => s.id !== scenarioId); + localStorage.setItem( + STORAGE_KEYS.SAVED_SCENARIOS, + JSON.stringify(updatedScenarios) + ); + return updatedScenarios; + }); + }, []); + + const value = { + settings, + showDevSettings, + savedScenarios, + updateSettings, + updateShowDevSettings, + resetSettings, + saveScenario, + deleteSavedScenario, + isDevelopment: process.env.NODE_ENV === "development", + }; + + return ( + + {children} + + ); +}; + +export const useSettings = () => { + const context = useContext(SettingsContext); + if (!context) { + throw new Error("useSettings must be used within a SettingsProvider"); + } + return context; +}; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 16c28e5..e69de29 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -1,65 +0,0 @@ -import { useState } from "react"; -import { PHYSICS_CONFIG } from "../constants/physics"; - -const STORAGE_KEYS = { - SETTINGS: "simulatorSettings", - SHOW_DEV: "showDevSettings", -} as const; - -export const useSettings = () => { - const [settings, setSettings] = useState(() => { - const savedSettings = localStorage.getItem(STORAGE_KEYS.SETTINGS); - if (savedSettings) { - const parsedSettings = JSON.parse(savedSettings); - const validSettings = Object.keys(parsedSettings).reduce((acc, key) => { - if (key in PHYSICS_CONFIG) { - acc[key] = parsedSettings[key]; - } - return acc; - }, {} as Partial); - - return { ...PHYSICS_CONFIG, ...validSettings }; - } - return PHYSICS_CONFIG; - }); - - const [showDevSettings, setShowDevSettings] = useState(() => { - const saved = localStorage.getItem(STORAGE_KEYS.SHOW_DEV); - return saved ? JSON.parse(saved) : false; - }); - - const updateSettings = (newSettings: Partial) => { - const validNewSettings = Object.keys(newSettings).reduce((acc, key) => { - if (key in PHYSICS_CONFIG) { - acc[key] = newSettings[key as keyof typeof PHYSICS_CONFIG]; - } - return acc; - }, {} as Partial); - - const updatedSettings = { ...settings, ...validNewSettings }; - setSettings(updatedSettings); - localStorage.setItem( - STORAGE_KEYS.SETTINGS, - JSON.stringify(updatedSettings) - ); - }; - - const updateShowDevSettings = (show: boolean) => { - setShowDevSettings(show); - localStorage.setItem(STORAGE_KEYS.SHOW_DEV, JSON.stringify(show)); - }; - - const resetSettings = () => { - setSettings(PHYSICS_CONFIG); - localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(PHYSICS_CONFIG)); - }; - - return { - settings, - showDevSettings, - updateSettings, - updateShowDevSettings, - resetSettings, - isDevelopment: process.env.NODE_ENV === "development", - }; -};