Skip to content

Commit c2e6d17

Browse files
committed
refactor: split <WorkspacePanel> into 3 internal components
1 parent 631cf98 commit c2e6d17

File tree

1 file changed

+174
-149
lines changed

1 file changed

+174
-149
lines changed

packages/components/react/src/Panels/WorkspacePanel.tsx

Lines changed: 174 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@ 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';
139
import { EditorPanel } from './EditorPanel.js';
14-
import { PreviewPanel, type ImperativePreviewHandle } from './PreviewPanel.js';
10+
import { PreviewPanel } from './PreviewPanel.js';
1511
import { TerminalPanel } from './TerminalPanel.js';
1612

1713
const DEFAULT_TERMINAL_SIZE = 25;
@@ -21,41 +17,80 @@ 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+
<Editor
45+
theme={theme}
46+
tutorialStore={tutorialStore}
47+
hasEditor={hasEditor}
48+
hasPreviews={hasPreviews}
49+
hideTerminalPanel={hideTerminalPanel}
50+
/>
4051

41-
const selectedFile = useStore(tutorialStore.selectedFile);
42-
const currentDocument = useStore(tutorialStore.currentDocument);
52+
<PanelResizeHandle
53+
className={resizePanelStyles.PanelResizeHandle}
54+
hitAreaMargins={{ fine: 5, coarse: 5 }}
55+
disabled={!hasEditor}
56+
/>
4357

44-
const lesson = tutorialStore.lesson!;
58+
<Preview
59+
theme={theme}
60+
tutorialStore={tutorialStore}
61+
terminalPanelRef={terminalPanelRef}
62+
terminalExpanded={terminalExpanded}
63+
hideTerminalPanel={hideTerminalPanel}
64+
hasPreviews={hasPreviews}
65+
hasEditor={hasEditor}
66+
/>
4567

46-
const onEditorChange = useCallback<OnEditorChange>((update) => {
47-
tutorialStore.setCurrentDocumentContent(update.content);
48-
}, []);
68+
<PanelResizeHandle
69+
className={resizePanelStyles.PanelResizeHandle}
70+
hitAreaMargins={{ fine: 5, coarse: 5 }}
71+
disabled={hideTerminalPanel || !hasPreviews}
72+
/>
4973

50-
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
51-
tutorialStore.setCurrentDocumentScrollPosition(position);
52-
}, []);
74+
<Terminal
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+
}
5386

54-
const onFileSelect = useCallback((filePath: string | undefined) => {
55-
tutorialStore.setSelectedFile(filePath);
56-
}, []);
87+
function Editor({ theme, tutorialStore, hasEditor }: PanelProps) {
88+
const [helpAction, setHelpAction] = useState<'solve' | 'reset'>('reset');
89+
const selectedFile = useStore(tutorialStore.selectedFile);
90+
const currentDocument = useStore(tutorialStore.currentDocument);
91+
const lesson = tutorialStore.lesson!;
5792

58-
const onHelpClick = useCallback(() => {
93+
function onHelpClick() {
5994
if (tutorialStore.hasSolution()) {
6095
setHelpAction((action) => {
6196
if (action === 'reset') {
@@ -71,157 +106,147 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
71106
} else {
72107
tutorialStore.reset();
73108
}
74-
}, [tutorialStore.ref]);
109+
}
75110

76111
useEffect(() => {
77-
const lesson = tutorialStore.lesson!;
78-
79-
const unsubscribe = tutorialStore.lessonFullyLoaded.subscribe((loaded) => {
80-
if (loaded && lesson.data.autoReload) {
81-
/**
82-
* @todo This causes some race with the preview where the iframe can show the "wrong" page.
83-
* I think the reason is that when the ports are different then we render new frames which
84-
* races against the reload which will internally reset the `src` attribute.
85-
*/
86-
// previewRef.current?.reload();
87-
}
88-
});
89-
90112
if (tutorialStore.hasSolution()) {
91113
setHelpAction('solve');
92114
} else {
93115
setHelpAction('reset');
94116
}
95-
96-
return () => unsubscribe();
97117
}, [tutorialStore.ref]);
98118

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

104-
terminalExpanded.current = false;
105-
}
106-
}, [hideTerminalPanel]);
147+
function Preview({
148+
tutorialStore,
149+
terminalPanelRef,
150+
terminalExpanded,
151+
hideTerminalPanel,
152+
hasPreviews,
153+
hasEditor,
154+
}: TerminalProps) {
155+
const lesson = tutorialStore.lesson!;
107156

108-
const showTerminal = useCallback(() => {
157+
const toggleTerminal = useCallback(() => {
109158
const { current: terminal } = terminalPanelRef;
110159

111160
if (!terminal) {
112161
return;
113162
}
114163

115-
if (!terminalExpanded.current) {
116-
terminalExpanded.current = true;
117-
terminal.resize(DEFAULT_TERMINAL_SIZE);
164+
if (terminalPanelRef.current?.isCollapsed()) {
165+
if (!terminalExpanded.current) {
166+
terminalExpanded.current = true;
167+
terminal.resize(DEFAULT_TERMINAL_SIZE);
168+
} else {
169+
terminal.expand();
170+
}
118171
} else {
119-
terminal.expand();
172+
terminalPanelRef.current?.collapse();
120173
}
121174
}, []);
122175

123-
const hideTerminal = useCallback(() => {
124-
terminalPanelRef.current?.collapse();
125-
}, []);
126-
127-
const toggleTerminal = useCallback(() => {
128-
const { current: terminal } = terminalPanelRef;
129-
130-
if (!terminal) {
131-
return;
132-
}
176+
useEffect(() => {
177+
if (hideTerminalPanel) {
178+
// force hide the terminal if we don't have any panels to show
179+
terminalPanelRef.current?.collapse();
133180

134-
if (terminalPanelRef.current?.isCollapsed()) {
135-
showTerminal();
136-
} else {
137-
hideTerminal();
181+
terminalExpanded.current = false;
138182
}
139-
}, []);
183+
}, [hideTerminalPanel]);
140184

141185
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={onHelpClick}
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}
186+
<Panel
187+
id={hasPreviews ? 'previews-opened' : 'previews-closed'}
188+
defaultSize={hasPreviews ? 50 : 0}
189+
minSize={10}
190+
maxSize={hasPreviews ? 100 : 0}
191+
collapsible={!hasPreviews}
192+
className={classNames({
193+
'transition-theme border-t border-tk-elements-app-borderColor': hasEditor,
194+
})}
195+
>
196+
<PreviewPanel
197+
tutorialStore={tutorialStore}
198+
i18n={lesson.data.i18n as I18n}
199+
showToggleTerminal={!hideTerminalPanel}
200+
toggleTerminal={toggleTerminal}
172201
/>
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}
196-
/>
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>
202+
</Panel>
203+
);
204+
}
205+
206+
function Terminal({
207+
tutorialStore,
208+
theme,
209+
terminalPanelRef,
210+
terminalExpanded,
211+
hideTerminalPanel,
212+
hasEditor,
213+
hasPreviews,
214+
}: TerminalProps) {
215+
let id = 'terminal-closed';
216+
217+
if (hideTerminalPanel) {
218+
id = 'terminal-none';
219+
} else if (!hasPreviews && !hasEditor) {
220+
id = 'terminal-full';
221+
} else if (!hasPreviews) {
222+
id = 'terminal-opened';
223+
}
224+
225+
let defaultSize = 0;
226+
227+
if (hideTerminalPanel) {
228+
defaultSize = 0;
229+
} else if (!hasPreviews && !hasEditor) {
230+
defaultSize = 100;
231+
} else if (!hasPreviews) {
232+
defaultSize = DEFAULT_TERMINAL_SIZE;
233+
}
234+
235+
return (
236+
<Panel
237+
id={id}
238+
defaultSize={defaultSize}
239+
minSize={hideTerminalPanel ? 0 : 10}
240+
collapsible={hasPreviews}
241+
ref={terminalPanelRef}
242+
onExpand={() => {
243+
terminalExpanded.current = true;
244+
}}
245+
className={classNames('transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor', {
246+
'border-t border-tk-elements-app-borderColor': hasPreviews,
247+
})}
248+
>
249+
<TerminalPanel tutorialStore={tutorialStore} theme={theme} />
250+
</Panel>
226251
);
227252
}

0 commit comments

Comments
 (0)