Skip to content

Commit 55618e9

Browse files
authored
Merge pull request #79 from bocan:refactor/preview
Implement Zustand store for content and scroll synchronization
2 parents 055f214 + 6afb37e commit 55618e9

7 files changed

Lines changed: 110 additions & 48 deletions

File tree

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"react-syntax-highlighter": "^16.1.0",
2323
"rehype-autolink-headings": "^7.1.0",
2424
"rehype-slug": "^6.0.0",
25-
"remark-gfm": "^4.0.0"
25+
"remark-gfm": "^4.0.0",
26+
"zustand": "^5.0.11"
2627
},
2728
"devDependencies": {
2829
"@testing-library/jest-dom": "^6.2.0",

client/src/App.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ function App() {
3636
const [error, setError] = useState<string | null>(null);
3737
const [leftPaneCollapsed, setLeftPaneCollapsed] = useState(false);
3838
const [rightPaneCollapsed, setRightPaneCollapsed] = useState(false);
39-
const [editorContent, setEditorContent] = useState<string>(""); // Track live editor content for preview
40-
const [scrollPercent, setScrollPercent] = useState<number>(0); // Synchronized scroll position (editor -> preview)
4139
const [isMobile, setIsMobile] = useState(false); // Track if we're on mobile for overlay behavior
4240
const [showAbout, setShowAbout] = useState(false); // About modal visibility
4341

@@ -318,13 +316,6 @@ function App() {
318316

319317
const handleSelectPage = (path: string) => {
320318
setSelectedPage(path);
321-
// Reset scroll position when changing pages
322-
setScrollPercent(0);
323-
};
324-
325-
// Handle scroll from editor - syncs to preview
326-
const handleEditorScroll = (percent: number) => {
327-
setScrollPercent(percent);
328319
};
329320

330321
const handleCloseEditor = () => {
@@ -620,8 +611,6 @@ function App() {
620611
<Editor
621612
pagePath={selectedPage}
622613
onClose={handleCloseEditor}
623-
onContentChange={setEditorContent}
624-
onScroll={handleEditorScroll}
625614
/>
626615
</section>
627616

@@ -656,9 +645,7 @@ function App() {
656645
<div className="pane-content">
657646
<Preview
658647
pagePath={selectedPage}
659-
liveContent={editorContent}
660648
onNavigate={handleSelectPage}
661-
scrollPercent={scrollPercent}
662649
/>
663650
</div>
664651
</>

client/src/components/Editor.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@ import React, { useState, useEffect, useRef } from "react";
22
import { api } from "../services/api";
33
import VersionHistory from "./VersionHistory";
44
import { Attachments } from "./Attachments";
5+
import { useEditorStore } from "../store/editorStore";
56
import "./Editor.css";
67

78
interface EditorProps {
89
pagePath: string | null;
910
onClose: () => void;
10-
onContentChange?: (content: string) => void;
11-
onScroll?: (percent: number) => void;
1211
}
1312

14-
export const Editor: React.FC<EditorProps> = ({
15-
pagePath,
16-
onClose,
17-
onContentChange,
18-
onScroll,
19-
}) => {
13+
export const Editor: React.FC<EditorProps> = ({ pagePath, onClose }) => {
14+
const { setContent: setStoreContent, scrollEditor } = useEditorStore();
2015
const [content, setContent] = useState("");
2116
const [isSaving, setIsSaving] = useState(false);
2217
const [isLoading, setIsLoading] = useState(false);
@@ -29,6 +24,7 @@ export const Editor: React.FC<EditorProps> = ({
2924
const [isListening, setIsListening] = useState(false);
3025
const textareaRef = useRef<HTMLTextAreaElement>(null);
3126
const scrollThrottleRef = useRef<number | null>(null);
27+
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
3228
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3329
const recognitionRef = useRef<any>(null);
3430
const contentRef = useRef<string>(content); // Track current content for speech recognition
@@ -38,11 +34,29 @@ export const Editor: React.FC<EditorProps> = ({
3834
contentRef.current = content;
3935
}, [content]);
4036

37+
// Debounced store update - only update preview after typing pauses
38+
useEffect(() => {
39+
if (debounceTimerRef.current) {
40+
clearTimeout(debounceTimerRef.current);
41+
}
42+
43+
debounceTimerRef.current = setTimeout(() => {
44+
setStoreContent(content);
45+
}, 600); // 600ms debounce
46+
47+
return () => {
48+
if (debounceTimerRef.current) {
49+
clearTimeout(debounceTimerRef.current);
50+
}
51+
};
52+
}, [content, setStoreContent]);
53+
4154
useEffect(() => {
4255
if (pagePath) {
4356
loadPage();
4457
} else {
4558
setContent("");
59+
setStoreContent("");
4660
setError(null);
4761
}
4862
}, [pagePath]);
@@ -55,10 +69,8 @@ export const Editor: React.FC<EditorProps> = ({
5569
try {
5670
const page = await api.getPage(pagePath);
5771
setContent(page.content);
58-
// Notify parent of initial content
59-
if (onContentChange) {
60-
onContentChange(page.content);
61-
}
72+
// Update store with initial content
73+
setStoreContent(page.content);
6274
} catch (err) {
6375
console.error("Failed to load page:", err);
6476
setError("Failed to load page. Click to retry.");
@@ -117,8 +129,6 @@ export const Editor: React.FC<EditorProps> = ({
117129

118130
// Handle editor scroll - sends scroll position to preview (throttled)
119131
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
120-
if (!onScroll) return;
121-
122132
const target = e.currentTarget;
123133
const scrollTop = target.scrollTop;
124134
const maxScroll = target.scrollHeight - target.clientHeight;
@@ -129,7 +139,7 @@ export const Editor: React.FC<EditorProps> = ({
129139
scrollThrottleRef.current = requestAnimationFrame(() => {
130140
scrollThrottleRef.current = null;
131141
if (maxScroll > 0) {
132-
onScroll(scrollTop / maxScroll);
142+
scrollEditor(scrollTop / maxScroll);
133143
}
134144
});
135145
};
@@ -174,9 +184,7 @@ export const Editor: React.FC<EditorProps> = ({
174184
currentContent.slice(end);
175185

176186
setContent(newContent);
177-
if (onContentChange) {
178-
onContentChange(newContent);
179-
}
187+
// Store update is debounced via useEffect
180188

181189
// Move cursor to end of inserted text
182190
const newCursorPos = start + finalTranscript.length;
@@ -241,9 +249,7 @@ export const Editor: React.FC<EditorProps> = ({
241249
const end = textarea.selectionEnd;
242250
const newContent = content.substring(0, start) + text + content.substring(end);
243251
setContent(newContent);
244-
if (onContentChange) {
245-
onContentChange(newContent);
246-
}
252+
// Store update is debounced via useEffect
247253
}
248254
};
249255

@@ -656,10 +662,7 @@ export const Editor: React.FC<EditorProps> = ({
656662
value={content}
657663
onChange={(e) => {
658664
setContent(e.target.value);
659-
// Notify parent of content change immediately for live preview
660-
if (onContentChange) {
661-
onContentChange(e.target.value);
662-
}
665+
// Store update is debounced via useEffect
663666
}}
664667
onScroll={handleScroll}
665668
onKeyDown={handleKeyDown}

client/src/components/Preview.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/pris
77
import { asBlob } from "html-docx-js-typescript";
88
import { api } from "../services/api";
99
import { TableOfContents } from "./TableOfContents";
10+
import { useEditorStore } from "../store/editorStore";
1011
import "./Preview.css";
1112

1213
// CodeBlock component with copy functionality
@@ -88,17 +89,16 @@ const CodeBlock: React.FC<{ language?: string; children: string }> = ({ language
8889

8990
interface PreviewProps {
9091
pagePath: string | null;
91-
liveContent?: string;
9292
onNavigate?: (path: string) => void;
93-
scrollPercent?: number;
9493
}
9594

9695
export const Preview: React.FC<PreviewProps> = ({
9796
pagePath,
98-
liveContent,
9997
onNavigate,
100-
scrollPercent,
10198
}) => {
99+
const liveContent = useEditorStore((state) => state.content);
100+
const scrollSource = useEditorStore((state) => state.scrollSource);
101+
const editorScrollPercent = useEditorStore((state) => state.editorScrollPercent);
102102
const [content, setContent] = useState("");
103103
const [loading, setLoading] = useState(false);
104104
const [showExportMenu, setShowExportMenu] = useState(false);
@@ -137,17 +137,17 @@ export const Preview: React.FC<PreviewProps> = ({
137137

138138
// Sync scroll from editor (smooth)
139139
useEffect(() => {
140-
if (contentRef.current && scrollPercent !== undefined) {
140+
if (contentRef.current && scrollSource === 'editor') {
141141
const container = contentRef.current;
142142
const maxScroll = container.scrollHeight - container.clientHeight;
143143
if (maxScroll > 0) {
144144
// Use requestAnimationFrame for smoother scrolling
145145
requestAnimationFrame(() => {
146-
container.scrollTop = maxScroll * scrollPercent;
146+
container.scrollTop = maxScroll * editorScrollPercent;
147147
});
148148
}
149149
}
150-
}, [scrollPercent]);
150+
}, [scrollSource, editorScrollPercent]);
151151

152152
// Close export menu when clicking outside
153153
useEffect(() => {

client/src/store/editorStore.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { create } from 'zustand';
2+
3+
interface EditorState {
4+
// Content state
5+
content: string;
6+
setContent: (content: string) => void;
7+
8+
// Scroll sync state
9+
scrollSource: 'editor' | 'preview' | null;
10+
editorScrollPercent: number;
11+
previewScrollPercent: number;
12+
13+
scrollEditor: (percent: number) => void;
14+
scrollPreview: (percent: number) => void;
15+
clearScrollSource: () => void;
16+
}
17+
18+
export const useEditorStore = create<EditorState>((set) => ({
19+
// Content
20+
content: '',
21+
setContent: (content) => set({ content }),
22+
23+
// Scroll sync
24+
scrollSource: null,
25+
editorScrollPercent: 0,
26+
previewScrollPercent: 0,
27+
28+
scrollEditor: (percent) =>
29+
set({
30+
scrollSource: 'editor',
31+
editorScrollPercent: percent,
32+
}),
33+
34+
scrollPreview: (percent) =>
35+
set({
36+
scrollSource: 'preview',
37+
previewScrollPercent: percent,
38+
}),
39+
40+
clearScrollSource: () => set({ scrollSource: null }),
41+
}));

data/Codex Documentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
- 🔐 **Password Protection**: Simple password-based authentication to secure your data
3939
- 🛡️ **Security Features**: Rate limiting, request logging, and security headers
4040

41-
## 🔐 Security & Logging
41+
## 🔐 Security & Logging.
4242

4343
Codex includes several security features to protect your data:
4444

package-lock.json

Lines changed: 31 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)