Skip to content

Commit 9bb4e58

Browse files
committed
Unifies button components and expands parameter selection
Consolidates button functionality into reusable components Replaces free-text parameter input with a selectable dropdown Introduces collapsible sections for cleaner metadata editing Updates and relocates the scripting schema with new fields Improves UI clarity for step logic configuration
1 parent 7b8cf6e commit 9bb4e58

15 files changed

+1260
-125
lines changed

editor/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>TrackMan Script Editor</title>
7+
<link rel="icon" type="image/x-icon" href="https://golf-portal-dev.trackmangolfdev.com/favicon.ico" />
78
<link rel="stylesheet" href="/src/index.css" />
89
</head>
910
<body>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, { useState, ReactNode, useEffect } from 'react';
2+
3+
interface CollapsibleSectionProps {
4+
title: string;
5+
defaultOpen?: boolean;
6+
children: ReactNode;
7+
className?: string; // extra wrapper classes
8+
bodyClassName?: string; // inner body classes
9+
persistKey?: string; // unique key to isolate state (e.g., node id + section name)
10+
}
11+
12+
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
13+
title,
14+
defaultOpen = true,
15+
children,
16+
className = '',
17+
bodyClassName = '',
18+
persistKey
19+
}) => {
20+
// localStorage key if provided so each unique section instance can have independent remembered state
21+
const storageKey = persistKey ? `collapsible:${persistKey}` : null;
22+
const initial = () => {
23+
if (storageKey && typeof window !== 'undefined') {
24+
try {
25+
const v = window.localStorage.getItem(storageKey);
26+
if (v === 'open') return true;
27+
if (v === 'closed') return false;
28+
} catch {
29+
/* ignore */
30+
}
31+
}
32+
return defaultOpen;
33+
};
34+
const [open, setOpen] = useState<boolean>(initial);
35+
36+
useEffect(() => {
37+
if (storageKey && typeof window !== 'undefined') {
38+
try {
39+
window.localStorage.setItem(storageKey, open ? 'open' : 'closed');
40+
} catch {
41+
/* ignore */
42+
}
43+
}
44+
}, [open, storageKey]);
45+
46+
const sectionId = `sect-${title.replace(/\s+/g,'-').toLowerCase()}-${persistKey || 'default'}`;
47+
48+
return (
49+
<div className={`collapsible ${open ? 'open' : 'closed'} ${className}`.trim()}>
50+
<button
51+
type="button"
52+
className="collapsible-header"
53+
onClick={() => setOpen(o => !o)}
54+
aria-controls={sectionId}
55+
aria-label={`${open ? 'Collapse' : 'Expand'} section ${title}`}
56+
>
57+
<svg
58+
className={`dropdown-arrow ${open ? 'open' : ''}`}
59+
width="12"
60+
height="12"
61+
viewBox="0 0 12 12"
62+
fill="currentColor"
63+
aria-hidden="true"
64+
>
65+
<path d="M6 9L1.5 4.5L2.5 3.5L6 7L9.5 3.5L10.5 4.5L6 9Z" />
66+
</svg>
67+
<span className="title-text">{title}</span>
68+
</button>
69+
<div
70+
id={sectionId}
71+
className={`collapsible-body ${open ? '' : 'hidden'}`.trim()}
72+
data-expanded={open ? 'true' : 'false'}
73+
>
74+
<div className={bodyClassName}>{children}</div>
75+
</div>
76+
</div>
77+
);
78+
};

editor/src/components/ConditionEditor.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,28 @@ export const ConditionEditor: React.FC<ConditionEditorProps> = ({ label, conditi
4949
{(c.conditions || []).length === 0 && <div className="cond-empty">No parameter constraints</div>}
5050
{(c.conditions || []).map((row, i) => (
5151
<div key={i} className="cond-row">
52-
<input className="cond-input" placeholder="parameter" value={row.parameter || ''} onChange={e => updateConditionRow(i, { parameter: e.target.value })} />
52+
<select className="cond-input" value={row.parameter || ''} onChange={e => updateConditionRow(i, { parameter: e.target.value })} aria-label="Parameter">
53+
<option value="">Select parameter...</option>
54+
<option value="AttackAngle">AttackAngle</option>
55+
<option value="BallSpeed">BallSpeed</option>
56+
<option value="CarryDistance">CarryDistance</option>
57+
<option value="ClubPath">ClubPath</option>
58+
<option value="ClubSpeed">ClubSpeed</option>
59+
<option value="Curve">Curve</option>
60+
<option value="DynamicLoft">DynamicLoft</option>
61+
<option value="FaceAngle">FaceAngle</option>
62+
<option value="FaceToPath">FaceToPath</option>
63+
<option value="FromPin">FromPin</option>
64+
<option value="Height">Height</option>
65+
<option value="LandingAngle">LandingAngle</option>
66+
<option value="LaunchAngle">LaunchAngle</option>
67+
<option value="LaunchDirection">LaunchDirection</option>
68+
<option value="Smash">Smash</option>
69+
<option value="Spin">Spin</option>
70+
<option value="SpinAxis">SpinAxis</option>
71+
<option value="StrokesGained">StrokesGained</option>
72+
<option value="Total">Total</option>
73+
</select>
5374
<input className="cond-input-narrow" placeholder="min" type="number" value={row.min ?? ''} onChange={e => updateConditionRow(i, { min: e.target.value === '' ? undefined : Number(e.target.value) })} />
5475
<input className="cond-input-narrow" placeholder="max" type="number" value={row.max ?? ''} onChange={e => updateConditionRow(i, { max: e.target.value === '' ? undefined : Number(e.target.value) })} />
5576
<button className="cond-remove" onClick={() => removeRow(i)}></button>

editor/src/components/EditPanel.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { Activity, Step, isActivity } from '../types';
3+
import { CollapsibleSection } from './CollapsibleSection';
34

45
interface EditPanelProps {
56
node: Activity | Step;
@@ -12,8 +13,14 @@ export const EditPanel: React.FC<EditPanelProps> = ({ node, onChange }) => {
1213
const endOrSuccess = isActivityNode ? (node as Activity).endMessage : (node as Step).successMessage;
1314

1415
return (
15-
<div className="edit-panel">
16-
<h3>Edit {isActivityNode ? 'Activity' : 'Step'} Metadata</h3>
16+
<CollapsibleSection
17+
key={`${isActivityNode ? 'activity' : 'step'}-${node.id}`}
18+
title={`Edit ${isActivityNode ? 'Activity' : 'Step'} Metadata`}
19+
className="edit-panel"
20+
bodyClassName="edit-panel-body"
21+
defaultOpen
22+
persistKey={`${node.id}-meta`}
23+
>
1724
<div className="edit-field">
1825
<label>ID <input value={node.id} onChange={e => onChange({ id: e.target.value })} /></label>
1926
</div>
@@ -33,6 +40,6 @@ export const EditPanel: React.FC<EditPanelProps> = ({ node, onChange }) => {
3340
<label>End Header <input value={endOrSuccess.header} onChange={e => onChange({ endMessage: { ...endOrSuccess, header: e.target.value } as any })} /></label>
3441
</div>
3542
)}
36-
</div>
43+
</CollapsibleSection>
3744
);
3845
};

editor/src/components/NodeDetailsPanel.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { Activity, Step, isActivity, isStep, ConditionGroup } from '../types';
33
import { ConditionEditor } from './ConditionEditor';
44
import { EditPanel } from './EditPanel';
5+
import { CollapsibleSection } from './CollapsibleSection';
56

67
interface NodeDetailsPanelProps {
78
selectedNode: Activity | Step | null;
@@ -30,15 +31,26 @@ export const NodeDetailsPanel: React.FC<NodeDetailsPanelProps> = ({
3031
<h2>Node Details</h2>
3132
{selectedNode ? (
3233
<>
34+
{/* For both activities and steps, show metadata first */}
35+
<EditPanel
36+
node={selectedNode}
37+
onChange={(patch: Partial<Activity> | Partial<Step>) => {
38+
if (isActivitySelected) {
39+
updateActivity(selectedNode.id, patch as Partial<Activity>);
40+
} else if (isStepSelected) {
41+
updateStep(selectedNode.id, patch as Partial<Step>);
42+
}
43+
}}
44+
/>
45+
{/* Step-only logic section follows metadata */}
3346
{isStepSelected && (selectedNode as Step).logic && (
34-
<div className="logic-block">
35-
<h3>Logic</h3>
47+
<CollapsibleSection title="Logic" className="logic-block" bodyClassName="logic-inner" defaultOpen persistKey={`${(selectedNode as Step).id}-logic`}>
3648
<ConditionEditor
3749
label="Success Condition"
3850
condition={(selectedNode as Step).logic.successCondition as ConditionGroup}
3951
showConditionType={true}
4052
onChange={(c) => {
41-
const s = selectedNode as Step;
53+
const s = selectedNode as Step;
4254
updateStep(s.id, { logic: { ...s.logic, successCondition: c } });
4355
}}
4456
/>
@@ -47,26 +59,16 @@ export const NodeDetailsPanel: React.FC<NodeDetailsPanelProps> = ({
4759
condition={(selectedNode as Step).logic.failCondition as ConditionGroup}
4860
showConditionType={true}
4961
onChange={(c) => {
50-
const s = selectedNode as Step;
62+
const s = selectedNode as Step;
5163
updateStep(s.id, { logic: { ...s.logic, failCondition: c } });
5264
}}
5365
/>
5466
<div className="logic-flags">
5567
<label>Can Retry: <input type="checkbox" checked={!!(selectedNode as Step).logic.canRetry} onChange={e => { const s = selectedNode as Step; updateStep(s.id, { logic: { ...s.logic, canRetry: e.target.checked } }); }} /></label>
5668
<label>Skip On Success: <input type="checkbox" checked={!!(selectedNode as Step).logic.skipOnSuccess} onChange={e => { const s = selectedNode as Step; updateStep(s.id, { logic: { ...s.logic, skipOnSuccess: e.target.checked } }); }} /></label>
5769
</div>
58-
</div>
70+
</CollapsibleSection>
5971
)}
60-
<EditPanel
61-
node={selectedNode}
62-
onChange={(patch: Partial<Activity> | Partial<Step>) => {
63-
if (isActivitySelected) {
64-
updateActivity(selectedNode.id, patch as Partial<Activity>);
65-
} else if (isStepSelected) {
66-
updateStep(selectedNode.id, patch as Partial<Step>);
67-
}
68-
}}
69-
/>
7072
<pre>{JSON.stringify(selectedNode, null, 2)}</pre>
7173
</>
7274
) : (

editor/src/components/Sidebar.tsx

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { ScriptData, Activity, Step } from '../types';
33
import { TreeView } from './TreeView';
44
import { BaySelector } from './BaySelector';
55
import { LocationSelector } from './LocationSelector';
6+
import {
7+
LoadScriptButton,
8+
DownloadButton,
9+
CloneSelectedButton,
10+
AddActivityButton,
11+
AddStepButton
12+
} from './buttons';
613

714
interface Bay {
815
id: string;
@@ -113,23 +120,8 @@ export const Sidebar: React.FC<SidebarProps> = ({
113120
<div className="file-operations-section">
114121
<h3 className="section-title">File Operations</h3>
115122
<div className="file-buttons">
116-
<button className="tree-btn load-btn" onClick={onLoadScript}>
117-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
118-
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
119-
</svg>
120-
Load Script
121-
</button>
122-
<button
123-
className="tree-btn download-btn"
124-
disabled={!isValid}
125-
onClick={onDownloadScript}
126-
title={!isValid ? "Fix validation errors before downloading" : "Download script"}
127-
>
128-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
129-
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
130-
</svg>
131-
Download
132-
</button>
123+
<LoadScriptButton onClick={onLoadScript} />
124+
<DownloadButton isValid={isValid} onClick={onDownloadScript} />
133125
</div>
134126
</div>
135127

@@ -168,36 +160,16 @@ export const Sidebar: React.FC<SidebarProps> = ({
168160
<div className="edit-operations-section">
169161
<h3 className="section-title">Edit Operations</h3>
170162
<div className="edit-buttons">
171-
<button
172-
className="tree-btn"
173-
disabled={!selectedNode || !isValid}
174-
onClick={onCloneSelected}
175-
>
176-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
177-
<path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/>
178-
</svg>
179-
Clone Selected
180-
</button>
181-
<button className="tree-btn" onClick={onShowActivityDialog}>
182-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
183-
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M11,15H13V17H15V15H17V13H15V11H13V13H11V15Z"/>
184-
</svg>
185-
Add Activity
186-
</button>
187-
<button
188-
className="tree-btn"
189-
disabled={!parentActivityForAdd}
190-
onClick={() => {
191-
if (parentActivityForAdd) {
192-
onShowStepDialog();
193-
}
194-
}}
195-
>
196-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
197-
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/>
198-
</svg>
199-
Add Step
200-
</button>
163+
<CloneSelectedButton
164+
selectedNode={selectedNode}
165+
isValid={isValid}
166+
onClick={onCloneSelected}
167+
/>
168+
<AddActivityButton onClick={onShowActivityDialog} />
169+
<AddStepButton
170+
parentActivityForAdd={parentActivityForAdd}
171+
onShowStepDialog={onShowStepDialog}
172+
/>
201173
</div>
202174
</div>
203175
<TreeView
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
3+
interface AddActivityButtonProps {
4+
onClick: () => void;
5+
}
6+
7+
export const AddActivityButton: React.FC<AddActivityButtonProps> = ({ onClick }) => {
8+
return (
9+
<button className="tree-btn" onClick={onClick}>
10+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
11+
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M11,15H13V17H15V15H17V13H15V11H13V13H11V15Z"/>
12+
</svg>
13+
Add Activity
14+
</button>
15+
);
16+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import { Activity } from '../../types';
3+
4+
interface AddStepButtonProps {
5+
parentActivityForAdd: Activity | undefined;
6+
onShowStepDialog: () => void;
7+
}
8+
9+
export const AddStepButton: React.FC<AddStepButtonProps> = ({
10+
parentActivityForAdd,
11+
onShowStepDialog
12+
}) => {
13+
return (
14+
<button
15+
className="tree-btn"
16+
disabled={!parentActivityForAdd}
17+
onClick={() => {
18+
if (parentActivityForAdd) {
19+
onShowStepDialog();
20+
}
21+
}}
22+
>
23+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
24+
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/>
25+
</svg>
26+
Add Step
27+
</button>
28+
);
29+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
3+
interface CloneSelectedButtonProps {
4+
selectedNode: any;
5+
isValid: boolean;
6+
onClick: () => void;
7+
}
8+
9+
export const CloneSelectedButton: React.FC<CloneSelectedButtonProps> = ({
10+
selectedNode,
11+
isValid,
12+
onClick
13+
}) => {
14+
return (
15+
<button
16+
className="tree-btn"
17+
disabled={!selectedNode || !isValid}
18+
onClick={onClick}
19+
>
20+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
21+
<path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/>
22+
</svg>
23+
Clone Selected
24+
</button>
25+
);
26+
};

0 commit comments

Comments
 (0)