Skip to content

Commit e40188e

Browse files
authored
refactor: split <WorkspacePanel> into 3 internal components (#253)
1 parent 9754b26 commit e40188e

File tree

1 file changed

+191
-139
lines changed

1 file changed

+191
-139
lines changed

packages/react/src/Panels/WorkspacePanel.tsx

Lines changed: 191 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import { TutorialStore } from '@tutorialkit/runtime';
33
import type { I18n } from '@tutorialkit/types';
44
import { useCallback, useEffect, useRef, useState } from 'react';
55
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
6-
import type {
7-
OnChangeCallback as OnEditorChange,
8-
OnScrollCallback as OnEditorScroll,
9-
} from '../core/CodeMirrorEditor/index.js';
106
import type { Theme } from '../core/types.js';
117
import resizePanelStyles from '../styles/resize-panel.module.css';
128
import { classNames } from '../utils/classnames.js';
@@ -21,42 +17,82 @@ interface Props {
2117
theme: Theme;
2218
}
2319

20+
interface PanelProps extends Props {
21+
hasEditor: boolean;
22+
hasPreviews: boolean;
23+
hideTerminalPanel: boolean;
24+
}
25+
26+
interface TerminalProps extends PanelProps {
27+
terminalPanelRef: React.RefObject<ImperativePanelHandle>;
28+
terminalExpanded: React.MutableRefObject<boolean>;
29+
}
30+
2431
/**
2532
* This component is the orchestrator between various interactive components.
2633
*/
2734
export function WorkspacePanel({ tutorialStore, theme }: Props) {
28-
const fileTree = tutorialStore.hasFileTree();
2935
const hasEditor = tutorialStore.hasEditor();
3036
const hasPreviews = tutorialStore.hasPreviews();
3137
const hideTerminalPanel = !tutorialStore.hasTerminalPanel();
3238

33-
const editorPanelRef = useRef<ImperativePanelHandle>(null);
34-
const previewPanelRef = useRef<ImperativePanelHandle>(null);
3539
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
36-
const previewRef = useRef<ImperativePreviewHandle>(null);
3740
const terminalExpanded = useRef(false);
3841

39-
const [helpAction, setHelpAction] = useState<'solve' | 'reset'>('reset');
42+
return (
43+
<PanelGroup className={resizePanelStyles.PanelGroup} direction="vertical">
44+
<EditorSection
45+
theme={theme}
46+
tutorialStore={tutorialStore}
47+
hasEditor={hasEditor}
48+
hasPreviews={hasPreviews}
49+
hideTerminalPanel={hideTerminalPanel}
50+
/>
51+
52+
<PanelResizeHandle
53+
className={resizePanelStyles.PanelResizeHandle}
54+
hitAreaMargins={{ fine: 5, coarse: 5 }}
55+
disabled={!hasEditor}
56+
/>
57+
58+
<PreviewsSection
59+
theme={theme}
60+
tutorialStore={tutorialStore}
61+
terminalPanelRef={terminalPanelRef}
62+
terminalExpanded={terminalExpanded}
63+
hideTerminalPanel={hideTerminalPanel}
64+
hasPreviews={hasPreviews}
65+
hasEditor={hasEditor}
66+
/>
67+
68+
<PanelResizeHandle
69+
className={resizePanelStyles.PanelResizeHandle}
70+
hitAreaMargins={{ fine: 5, coarse: 5 }}
71+
disabled={hideTerminalPanel || !hasPreviews}
72+
/>
73+
74+
<TerminalSection
75+
tutorialStore={tutorialStore}
76+
theme={theme}
77+
terminalPanelRef={terminalPanelRef}
78+
terminalExpanded={terminalExpanded}
79+
hideTerminalPanel={hideTerminalPanel}
80+
hasEditor={hasEditor}
81+
hasPreviews={hasPreviews}
82+
/>
83+
</PanelGroup>
84+
);
85+
}
4086

87+
function EditorSection({ theme, tutorialStore, hasEditor }: PanelProps) {
88+
const [helpAction, setHelpAction] = useState<'solve' | 'reset'>('reset');
4189
const selectedFile = useStore(tutorialStore.selectedFile);
4290
const currentDocument = useStore(tutorialStore.currentDocument);
4391
const lessonFullyLoaded = useStore(tutorialStore.lessonFullyLoaded);
4492

4593
const lesson = tutorialStore.lesson!;
4694

47-
const onEditorChange = useCallback<OnEditorChange>((update) => {
48-
tutorialStore.setCurrentDocumentContent(update.content);
49-
}, []);
50-
51-
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
52-
tutorialStore.setCurrentDocumentScrollPosition(position);
53-
}, []);
54-
55-
const onFileSelect = useCallback((filePath: string | undefined) => {
56-
tutorialStore.setSelectedFile(filePath);
57-
}, []);
58-
59-
const onHelpClick = useCallback(() => {
95+
function onHelpClick() {
6096
if (tutorialStore.hasSolution()) {
6197
setHelpAction((action) => {
6298
if (action === 'reset') {
@@ -72,40 +108,57 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
72108
} else {
73109
tutorialStore.reset();
74110
}
75-
}, [tutorialStore.ref]);
111+
}
76112

77113
useEffect(() => {
78-
const lesson = tutorialStore.lesson!;
79-
80-
const unsubscribe = tutorialStore.lessonFullyLoaded.subscribe((loaded) => {
81-
if (loaded && lesson.data.autoReload) {
82-
previewRef.current?.reload();
83-
}
84-
});
85-
86114
if (tutorialStore.hasSolution()) {
87115
setHelpAction('solve');
88116
} else {
89117
setHelpAction('reset');
90118
}
91-
92-
if (tutorialStore.terminalConfig.value?.defaultOpen) {
93-
showTerminal();
94-
}
95-
96-
return () => unsubscribe();
97119
}, [tutorialStore.ref]);
98120

99-
useEffect(() => {
100-
if (hideTerminalPanel) {
101-
// force hide the terminal if we don't have any panels to show
102-
hideTerminal();
121+
return (
122+
<Panel
123+
id={hasEditor ? 'editor-opened' : 'editor-closed'}
124+
defaultSize={hasEditor ? 50 : 0}
125+
minSize={10}
126+
maxSize={hasEditor ? 100 : 0}
127+
collapsible={!hasEditor}
128+
className="transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor"
129+
>
130+
<EditorPanel
131+
id={tutorialStore.ref}
132+
theme={theme}
133+
showFileTree={tutorialStore.hasFileTree()}
134+
editorDocument={currentDocument}
135+
files={lesson.files[1]}
136+
i18n={lesson.data.i18n as I18n}
137+
hideRoot={lesson.data.hideRoot}
138+
helpAction={helpAction}
139+
onHelpClick={lessonFullyLoaded ? onHelpClick : undefined}
140+
onFileSelect={(filePath) => tutorialStore.setSelectedFile(filePath)}
141+
selectedFile={selectedFile}
142+
onEditorScroll={(position) => tutorialStore.setCurrentDocumentScrollPosition(position)}
143+
onEditorChange={(update) => tutorialStore.setCurrentDocumentContent(update.content)}
144+
/>
145+
</Panel>
146+
);
147+
}
103148

104-
terminalExpanded.current = false;
105-
}
106-
}, [hideTerminalPanel]);
149+
function PreviewsSection({
150+
tutorialStore,
151+
terminalPanelRef,
152+
terminalExpanded,
153+
hideTerminalPanel,
154+
hasPreviews,
155+
hasEditor,
156+
}: TerminalProps) {
157+
const previewRef = useRef<ImperativePreviewHandle>(null);
158+
const lesson = tutorialStore.lesson!;
159+
const terminalConfig = useStore(tutorialStore.terminalConfig);
107160

108-
const showTerminal = useCallback(() => {
161+
function showTerminal() {
109162
const { current: terminal } = terminalPanelRef;
110163

111164
if (!terminal) {
@@ -118,110 +171,109 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
118171
} else {
119172
terminal.expand();
120173
}
121-
}, []);
174+
}
122175

123-
const hideTerminal = useCallback(() => {
124-
terminalPanelRef.current?.collapse();
176+
const toggleTerminal = useCallback(() => {
177+
if (terminalPanelRef.current?.isCollapsed()) {
178+
showTerminal();
179+
} else if (terminalPanelRef.current) {
180+
terminalPanelRef.current.collapse();
181+
}
125182
}, []);
126183

127-
const toggleTerminal = useCallback(() => {
128-
const { current: terminal } = terminalPanelRef;
184+
useEffect(() => {
185+
if (hideTerminalPanel) {
186+
// force hide the terminal if we don't have any panels to show
187+
terminalPanelRef.current?.collapse();
129188

130-
if (!terminal) {
131-
return;
189+
terminalExpanded.current = false;
132190
}
191+
}, [hideTerminalPanel]);
133192

134-
if (terminalPanelRef.current?.isCollapsed()) {
193+
useEffect(() => {
194+
if (terminalConfig.defaultOpen) {
135195
showTerminal();
136-
} else {
137-
hideTerminal();
138196
}
139-
}, []);
197+
}, [terminalConfig.defaultOpen]);
198+
199+
useEffect(() => {
200+
const lesson = tutorialStore.lesson!;
201+
202+
const unsubscribe = tutorialStore.lessonFullyLoaded.subscribe((loaded) => {
203+
if (loaded && lesson.data.autoReload) {
204+
previewRef.current?.reload();
205+
}
206+
});
207+
208+
return () => unsubscribe();
209+
}, [tutorialStore.ref]);
140210

141211
return (
142-
<PanelGroup className={resizePanelStyles.PanelGroup} direction="vertical">
143-
<Panel
144-
id={hasEditor ? 'editor-opened' : 'editor-closed'}
145-
defaultSize={hasEditor ? 50 : 0}
146-
minSize={10}
147-
maxSize={hasEditor ? 100 : 0}
148-
collapsible={!hasEditor}
149-
ref={editorPanelRef}
150-
className="transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor"
151-
>
152-
<EditorPanel
153-
id={tutorialStore.ref}
154-
theme={theme}
155-
showFileTree={fileTree}
156-
editorDocument={currentDocument}
157-
files={lesson.files[1]}
158-
i18n={lesson.data.i18n as I18n}
159-
hideRoot={lesson.data.hideRoot}
160-
helpAction={helpAction}
161-
onHelpClick={lessonFullyLoaded ? onHelpClick : undefined}
162-
onFileSelect={onFileSelect}
163-
selectedFile={selectedFile}
164-
onEditorScroll={onEditorScroll}
165-
onEditorChange={onEditorChange}
166-
/>
167-
</Panel>
168-
<PanelResizeHandle
169-
className={resizePanelStyles.PanelResizeHandle}
170-
hitAreaMargins={{ fine: 5, coarse: 5 }}
171-
disabled={!hasEditor}
172-
/>
173-
<Panel
174-
id={hasPreviews ? 'previews-opened' : 'previews-closed'}
175-
defaultSize={hasPreviews ? 50 : 0}
176-
minSize={10}
177-
maxSize={hasPreviews ? 100 : 0}
178-
collapsible={!hasPreviews}
179-
ref={previewPanelRef}
180-
className={classNames({
181-
'transition-theme border-t border-tk-elements-app-borderColor': hasEditor,
182-
})}
183-
>
184-
<PreviewPanel
185-
tutorialStore={tutorialStore}
186-
i18n={lesson.data.i18n as I18n}
187-
ref={previewRef}
188-
showToggleTerminal={!hideTerminalPanel}
189-
toggleTerminal={toggleTerminal}
190-
/>
191-
</Panel>
192-
<PanelResizeHandle
193-
className={resizePanelStyles.PanelResizeHandle}
194-
hitAreaMargins={{ fine: 5, coarse: 5 }}
195-
disabled={hideTerminalPanel || !hasPreviews}
212+
<Panel
213+
id={hasPreviews ? 'previews-opened' : 'previews-closed'}
214+
defaultSize={hasPreviews ? 50 : 0}
215+
minSize={10}
216+
maxSize={hasPreviews ? 100 : 0}
217+
collapsible={!hasPreviews}
218+
className={classNames({
219+
'transition-theme border-t border-tk-elements-app-borderColor': hasEditor,
220+
})}
221+
>
222+
<PreviewPanel
223+
ref={previewRef}
224+
tutorialStore={tutorialStore}
225+
i18n={lesson.data.i18n as I18n}
226+
showToggleTerminal={!hideTerminalPanel}
227+
toggleTerminal={toggleTerminal}
196228
/>
197-
<Panel
198-
id={
199-
hideTerminalPanel
200-
? 'terminal-none'
201-
: !hasPreviews && !hasEditor
202-
? 'terminal-full'
203-
: !hasPreviews
204-
? 'terminal-opened'
205-
: 'terminal-closed'
206-
}
207-
defaultSize={
208-
hideTerminalPanel ? 0 : !hasPreviews && !hasEditor ? 100 : !hasPreviews ? DEFAULT_TERMINAL_SIZE : 0
209-
}
210-
minSize={hideTerminalPanel ? 0 : 10}
211-
collapsible={hasPreviews}
212-
ref={terminalPanelRef}
213-
onExpand={() => {
214-
terminalExpanded.current = true;
215-
}}
216-
className={classNames(
217-
'transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor',
218-
{
219-
'border-t border-tk-elements-app-borderColor': hasPreviews,
220-
},
221-
)}
222-
>
223-
<TerminalPanel tutorialStore={tutorialStore} theme={theme} />
224-
</Panel>
225-
</PanelGroup>
229+
</Panel>
230+
);
231+
}
232+
233+
function TerminalSection({
234+
tutorialStore,
235+
theme,
236+
terminalPanelRef,
237+
terminalExpanded,
238+
hideTerminalPanel,
239+
hasEditor,
240+
hasPreviews,
241+
}: TerminalProps) {
242+
let id = 'terminal-closed';
243+
244+
if (hideTerminalPanel) {
245+
id = 'terminal-none';
246+
} else if (!hasPreviews && !hasEditor) {
247+
id = 'terminal-full';
248+
} else if (!hasPreviews) {
249+
id = 'terminal-opened';
250+
}
251+
252+
let defaultSize = 0;
253+
254+
if (hideTerminalPanel) {
255+
defaultSize = 0;
256+
} else if (!hasPreviews && !hasEditor) {
257+
defaultSize = 100;
258+
} else if (!hasPreviews) {
259+
defaultSize = DEFAULT_TERMINAL_SIZE;
260+
}
261+
262+
return (
263+
<Panel
264+
id={id}
265+
defaultSize={defaultSize}
266+
minSize={hideTerminalPanel ? 0 : 10}
267+
collapsible={hasPreviews}
268+
ref={terminalPanelRef}
269+
onExpand={() => {
270+
terminalExpanded.current = true;
271+
}}
272+
className={classNames('transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor', {
273+
'border-t border-tk-elements-app-borderColor': hasPreviews,
274+
})}
275+
>
276+
<TerminalPanel tutorialStore={tutorialStore} theme={theme} />
277+
</Panel>
226278
);
227279
}

0 commit comments

Comments
 (0)