Skip to content

Commit

Permalink
Saving scenarios (#24)
Browse files Browse the repository at this point in the history
* basic working saveing

* wrap in context replacing useSettings
  • Loading branch information
yhattav authored Nov 26, 2024
1 parent 36c0f79 commit c28c35d
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 134 deletions.
105 changes: 54 additions & 51 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -41,59 +42,61 @@ function App() {
}, []);

return (
<Layout className="app-layout">
<Header className="app-header">
<div className="header-content">
<h1 className="app-title">Gravity Simulator</h1>
<div className="header-icons">
<a
href="https://github.com/yhattav/react-gravity"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="View Source Code"
>
<GithubOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/issues"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Report Issues"
>
<BugOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/wiki"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Documentation"
>
<QuestionCircleOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/blob/main/README.md#configuration"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Configuration Guide"
>
<SettingOutlined />
</a>
<SettingsProvider>
<Layout className="app-layout">
<Header className="app-header">
<div className="header-content">
<h1 className="app-title">Gravity Simulator</h1>
<div className="header-icons">
<a
href="https://github.com/yhattav/react-gravity"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="View Source Code"
>
<GithubOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/issues"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Report Issues"
>
<BugOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/wiki"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Documentation"
>
<QuestionCircleOutlined />
</a>
<a
href="https://github.com/yhattav/react-gravity/blob/main/README.md#configuration"
target="_blank"
rel="noopener noreferrer"
className="header-icon"
title="Configuration Guide"
>
<SettingOutlined />
</a>
</div>
</div>
</div>
</Header>
<Layout>
<Content className="app-content">
<GravitySection onDebugData={handleDebugData} />
</Content>
<Layout.Sider className="app-sider" width="20%">
{debugData && <DebugInfo data={debugData} />}
</Layout.Sider>
</Header>
<Layout>
<Content className="app-content">
<GravitySection onDebugData={handleDebugData} />
</Content>
<Layout.Sider className="app-sider" width="20%">
{debugData && <DebugInfo data={debugData} />}
</Layout.Sider>
</Layout>
</Layout>
</Layout>
</SettingsProvider>
);
}

Expand Down
46 changes: 35 additions & 11 deletions src/components/GravitySimulator/GravitySimulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -81,12 +82,17 @@ export const GravitySimulator: React.FC<GravitySimulatorProps> = ({
);
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) {
Expand Down Expand Up @@ -342,17 +348,29 @@ export const GravitySimulator: React.FC<GravitySimulatorProps> = ({
});
}, []);

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

Check failure on line 366 in src/components/GravitySimulator/GravitySimulator.tsx

View workflow job for this annotation

GitHub Actions / build

'trails' is defined but never used

Check failure on line 366 in src/components/GravitySimulator/GravitySimulator.tsx

View workflow job for this annotation

GitHub Actions / build

'force' is defined but never used
),
},
};
console.log(JSON.stringify(scenario, null, 2));
saveScenario(scenario);
setIsSaveModalOpen(false);
},
[physicsConfig, gravityPoints, particles]
[physicsConfig, gravityPoints, particles, saveScenario]
);

const handleSelectScenario = useCallback(
Expand Down Expand Up @@ -602,6 +620,12 @@ export const GravitySimulator: React.FC<GravitySimulatorProps> = ({
onSelectScenario={handleSelectScenario}
/>

<SaveScenarioModal
isOpen={isSaveModalOpen}
onClose={() => setIsSaveModalOpen(false)}
onSave={handleSaveScenario}
/>

<a
href="https://github.com/yhattav"
target="_blank"
Expand Down
92 changes: 92 additions & 0 deletions src/components/SaveScenarioModal/SaveScenarioModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Scenario } from "../../types/scenario";

Check failure on line 3 in src/components/SaveScenarioModal/SaveScenarioModal.tsx

View workflow job for this annotation

GitHub Actions / build

'Scenario' is defined but never used

interface SaveScenarioModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (name: string) => void;
}

export const SaveScenarioModal: React.FC<SaveScenarioModalProps> = ({
isOpen,
onClose,
onSave,
}) => {
const [scenarioName, setScenarioName] = useState("");

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (scenarioName.trim()) {
onSave(scenarioName);
setScenarioName("");
}
};

return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ type: "spring", damping: 20, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
className="floating-panel"
style={{
position: "absolute",
top: "70px",
right: "20px",
width: "280px",
padding: "20px",
color: "rgba(255, 255, 255, 0.9)",
}}
>
<h3 style={{ margin: "0 0 15px 0", fontSize: "1rem" }}>
Save Scenario
</h3>
<form onSubmit={handleSubmit}>
<input
type="text"
value={scenarioName}
onChange={(e) => 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",
}}
/>
<div
style={{
display: "flex",
gap: "10px",
justifyContent: "flex-end",
}}
>
<button
type="button"
onClick={onClose}
className="action-button"
style={{ background: "rgba(255, 255, 255, 0.1)" }}
>
Cancel
</button>
<button
type="submit"
className="action-button"
style={{ background: "rgba(78, 205, 196, 0.2)" }}
>
Save
</button>
</div>
</form>
</motion.div>
)}
</AnimatePresence>
);
};
65 changes: 59 additions & 6 deletions src/components/ScenarioPanel/ScenarioPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +18,7 @@ export const ScenarioPanel: React.FC<ScenarioPanelProps> = ({
onClose,

Check failure on line 18 in src/components/ScenarioPanel/ScenarioPanel.tsx

View workflow job for this annotation

GitHub Actions / build

'onClose' is defined but never used
}) => {
const [activeTab, setActiveTab] = useState("1");
const { savedScenarios, deleteSavedScenario } = useSettings();

return (
<AnimatePresence>
Expand Down Expand Up @@ -127,11 +128,63 @@ export const ScenarioPanel: React.FC<ScenarioPanelProps> = ({
</span>
),
children: (
<div
className="scenarios-list"
style={{ color: "rgba(255, 255, 255, 0.7)" }}
>
Coming soon...
<div className="scenarios-list">
{savedScenarios.length === 0 ? (
<div style={{ color: "rgba(255, 255, 255, 0.7)" }}>
No saved scenarios yet
</div>
) : (
savedScenarios.map((scenario) => (
<motion.div
key={scenario.id}
className="scenario-item"
onClick={() => onSelectScenario(scenario)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h3
style={{
fontSize: "0.9rem",
color: "rgba(255, 255, 255, 0.9)",
fontWeight: "normal",
}}
>
{scenario.name}
</h3>
<button
onClick={(e) => {
e.stopPropagation();
deleteSavedScenario(scenario.id);
}}
style={{
background: "none",
border: "none",
color: "rgba(255, 255, 255, 0.5)",
cursor: "pointer",
padding: "4px",
}}
>
×
</button>
</div>
<p
style={{
fontSize: "0.85rem",
color: "rgba(255, 255, 255, 0.7)",
}}
>
{scenario.description}
</p>
</motion.div>
))
)}
</div>
),
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/SimulatorSettings/SimulatorSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from "react";

Check failure on line 1 in src/components/SimulatorSettings/SimulatorSettings.tsx

View workflow job for this annotation

GitHub Actions / build

'useState' is defined but never used
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;
Expand Down
Loading

0 comments on commit c28c35d

Please sign in to comment.