• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jcubic / 10xDevs / 18619660514

18 Oct 2025 06:52PM UTC coverage: 19.299% (-2.7%) from 21.953%
18619660514

push

github

jcubic
improve performance

77 of 110 branches covered (70.0%)

Branch coverage included in aggregate %.

35 of 162 new or added lines in 4 files covered. (21.6%)

189 existing lines in 8 files now uncovered.

468 of 2714 relevant lines covered (17.24%)

2.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.39
/src/components/notes/MainNotesClient.tsx
1
'use client';
1✔
2

3
import { useEffect, useMemo, useRef, useCallback, useState } from 'react';
×
4
import { useRouter } from 'next/navigation';
×
5
import type { NoteTreeNode } from '@/types/tree';
6
import { useNotesContext } from './NotesContext';
×
7
import { createNote, updateNote, deleteNote } from '@/app/actions/notes';
×
8
import { extractHeaders } from '@/lib/parser/markdown-parser';
×
9
import TopNavigationBar from './TopNavigationBar';
×
10
import LeftPanel from './LeftPanel';
×
11
import MiddlePanel from './MiddlePanel';
×
12
import RightPanel from './RightPanel';
×
13
import Footer from '@/components/Footer';
×
14

15
import styles from './MainNotesLayout.module.css';
×
16

17
interface MainNotesClientProps {
18
  lineNumber?: number;
19
}
20

21
export default function MainNotesClient({ lineNumber }: MainNotesClientProps) {
×
22
  const router = useRouter();
×
23

24
  const {
×
25
    notes,
×
26
    selectedNoteId,
×
27
    saveStatus,
×
28
    setSaveStatus,
×
29
    updateNoteContent,
×
30
    updateNoteName,
×
31
    markNoteDirty,
×
32
    getSelectedNote,
×
33
    setNotes
×
UNCOV
34
  } = useNotesContext();
×
35

36
  const editorRef = useRef<import('@/types/editor').EditorRef | null>(null);
×
UNCOV
37
  const [welcomeContent, setWelcomeContent] = useState<string>('');
×
UNCOV
38
  const [currentLine, setCurrentLine] = useState<number | undefined>(lineNumber);
×
39

40
  // Load welcome content when component mounts
41
  useEffect(() => {
×
42
    const loadWelcomeContent = async () => {
×
43
      try {
×
44
        const response = await fetch('/samples/welcome.md');
×
45
        const text = await response.text();
×
46
        setWelcomeContent(text);
×
47
      } catch (error) {
×
48
        console.error('Failed to load welcome content:', error);
×
49
        setWelcomeContent('# Welcome to SNApp\n\nStart writing your note...');
×
UNCOV
50
      }
×
51
    };
×
52

UNCOV
53
    loadWelcomeContent();
×
54
  }, []);
×
55

56
  // Sync currentLine with lineNumber prop changes
57
  useEffect(() => {
×
58
    setCurrentLine(lineNumber);
×
59
  }, [lineNumber]);
×
60

61
  // Monitor URL changes for line parameter
UNCOV
62
  useEffect(() => {
×
63
    const extractLineFromUrl = () => {
×
UNCOV
64
      const urlParams = new URLSearchParams(window.location.search);
×
65
      const lineParam = urlParams.get('line');
×
66

UNCOV
67
      if (lineParam) {
×
68
        const parsedLine = parseInt(lineParam, 10);
×
69
        if (!isNaN(parsedLine)) {
×
70
          setCurrentLine(parsedLine);
×
71
          return;
×
UNCOV
72
        }
×
73
      }
×
74

75
      setCurrentLine(undefined);
×
UNCOV
76
    };
×
77

78
    // Extract line on mount and URL changes
79
    extractLineFromUrl();
×
80

81
    const handlePopState = () => {
×
82
      extractLineFromUrl();
×
83
    };
×
84

85
    // Monitor navigation events
UNCOV
86
    const originalPushState = window.history.pushState;
×
87
    const originalReplaceState = window.history.replaceState;
×
88

89
    window.history.pushState = function (...args) {
×
90
      originalPushState.apply(this, args);
×
91
      setTimeout(extractLineFromUrl, 0);
×
92
    };
×
93

94
    window.history.replaceState = function (...args) {
×
95
      originalReplaceState.apply(this, args);
×
96
      setTimeout(extractLineFromUrl, 0);
×
97
    };
×
98

UNCOV
99
    window.addEventListener('popstate', handlePopState);
×
100

101
    return () => {
×
UNCOV
102
      window.removeEventListener('popstate', handlePopState);
×
103
      window.history.pushState = originalPushState;
×
104
      window.history.replaceState = originalReplaceState;
×
105
    };
×
106
  }, []);
×
107

UNCOV
108
  const selectedNote = getSelectedNote();
×
109
  // Use the same logic as MiddlePanel to determine the actual content being displayed
110
  const content =
×
111
    selectedNote?.data?.content === null
×
112
      ? welcomeContent
×
113
      : selectedNote?.data?.content || '';
×
114
  const hasUnsavedChanges = selectedNote?.data?.dirty || false;
×
115

116
  // Extract headers from current content (the actual content being displayed)
117
  const headers = useMemo(() => extractHeaders(content), [content]);
×
118

119
  // Memoize editor ready handler to prevent re-renders
NEW
120
  const handleEditorReady = useCallback((editor: import('@/types/editor').EditorRef) => {
×
NEW
121
    editorRef.current = editor;
×
NEW
122
  }, []);
×
123

124
  const handleNoteSelect = useCallback(
×
125
    (noteId: number) => {
×
126
      // Navigate to the selected note
127
      router.push(`/note/${noteId}`);
×
128
    },
×
129
    [router]
×
130
  );
×
131

132
  const handleNewNote = useCallback(async () => {
×
133
    try {
×
134
      const newNote = await createNote('New Note');
×
135

136
      // Add the new note to our local state
137
      const newTreeNode: NoteTreeNode = {
×
138
        id: newNote.id,
×
139
        name: newNote.name,
×
140
        data: {
×
UNCOV
141
          content: newNote.content || '',
×
UNCOV
142
          dirty: false
×
143
        }
×
144
      };
×
145

UNCOV
146
      setNotes((prevNotes: NoteTreeNode[]) => [newTreeNode, ...prevNotes]);
×
UNCOV
147
      router.push(`/note/${newNote.id}`);
×
148
    } catch (error) {
×
149
      console.error('Failed to create note:', error);
×
150
    }
×
151
  }, [setNotes, router]);
×
152

NEW
153
  const handleContentChange = useCallback(
×
NEW
154
    (newContent: string) => {
×
NEW
155
      if (selectedNoteId) {
×
156
        // Update local state immediately for responsiveness
NEW
157
        updateNoteContent(selectedNoteId, newContent);
×
158
        // Manual save only - no automatic background saving
NEW
159
      }
×
NEW
160
    },
×
NEW
161
    [selectedNoteId, updateNoteContent]
×
NEW
162
  );
×
163

164
  const handleSave = useCallback(async () => {
×
165
    if (!selectedNote) return;
×
166

167
    try {
×
168
      setSaveStatus('saving');
×
169
      await updateNote(selectedNote.id, { content });
×
170
      setSaveStatus('saved');
×
171
      markNoteDirty(selectedNote.id, false);
×
172

173
      // Reset status after 2 seconds
174
      setTimeout(() => setSaveStatus('idle'), 2000);
×
UNCOV
175
    } catch (error) {
×
176
      setSaveStatus('error');
×
177
      console.error('Failed to save note:', error);
×
UNCOV
178
    }
×
179
  }, [selectedNote, content, setSaveStatus, markNoteDirty]);
×
180

181
  const handleRefresh = useCallback(async () => {
×
182
    if (!selectedNote) return;
×
183

UNCOV
184
    try {
×
UNCOV
185
      setSaveStatus('saving');
×
186
      const freshNote = await updateNote(selectedNote.id, {}); // Fetch without changes
×
187
      updateNoteContent(selectedNote.id, freshNote.content || '');
×
188
      markNoteDirty(selectedNote.id, false);
×
189
      setSaveStatus('idle');
×
UNCOV
190
    } catch (error) {
×
UNCOV
191
      setSaveStatus('error');
×
192
      console.error('Failed to refresh note:', error);
×
193
    }
×
194
  }, [selectedNote, setSaveStatus, updateNoteContent, markNoteDirty]);
×
195

196
  const handleDeleteNote = useCallback(
×
197
    async (noteId: number) => {
×
198
      try {
×
199
        await deleteNote(noteId);
×
200

201
        // Remove from local state
202
        setNotes((prevNotes: NoteTreeNode[]) =>
×
203
          prevNotes.filter((note: NoteTreeNode) => note.id !== noteId)
×
204
        );
×
205

206
        // If we deleted the currently selected note, navigate home
207
        if (selectedNoteId === noteId) {
×
208
          router.push('/');
×
209
        }
×
210
      } catch (error) {
×
211
        console.error('Failed to delete note:', error);
×
212
      }
×
213
    },
×
UNCOV
214
    [setNotes, selectedNoteId, router]
×
215
  );
×
216

217
  const handleRenameNote = useCallback(
×
UNCOV
218
    async (noteId: number, newName: string) => {
×
UNCOV
219
      try {
×
220
        const updatedNote = await updateNote(noteId, { name: newName });
×
221
        updateNoteName(noteId, updatedNote.name);
×
222
      } catch (error) {
×
223
        console.error('Failed to rename note:', error);
×
224
        throw error; // Re-throw to let the component handle the error
×
225
      }
×
226
    },
×
227
    [updateNoteName]
×
UNCOV
228
  );
×
229

NEW
230
  const handleHeaderClick = useCallback(
×
NEW
231
    (line: number) => {
×
232
      // Update current line state immediately for visual feedback
NEW
233
      setCurrentLine(line);
×
234

NEW
235
      if (selectedNoteId) {
×
NEW
236
        const newUrl = `/note/${selectedNoteId}?line=${line}`;
×
NEW
237
        window.history.pushState({ noteId: selectedNoteId, line }, '', newUrl);
×
NEW
238
      }
×
239

NEW
240
      if (editorRef.current) {
×
NEW
241
        editorRef.current.scrollToLine(line);
×
NEW
242
      }
×
NEW
243
    },
×
NEW
244
    [selectedNoteId]
×
NEW
245
  );
×
246

247
  const handleLogout = () => {
×
248
    // TODO: Implement logout logic
249
    console.log('Logout clicked');
×
250
  };
×
251

252
  // Keyboard shortcuts
253
  useEffect(() => {
×
254
    const handleKeyDown = (e: KeyboardEvent) => {
×
255
      if (e.ctrlKey || e.metaKey) {
×
256
        switch (e.key) {
×
257
          case 's':
×
258
            e.preventDefault();
×
259
            handleSave();
×
260
            break;
×
261
          case 'n':
×
262
            e.preventDefault();
×
263
            handleNewNote();
×
264
            break;
×
265
          case 'r':
×
266
            // Only trigger refresh when focused in editor
UNCOV
267
            if (document.activeElement?.closest('[data-editor]')) {
×
268
              e.preventDefault();
×
UNCOV
269
              handleRefresh();
×
270
            }
×
UNCOV
271
            break;
×
UNCOV
272
        }
×
UNCOV
273
      }
×
UNCOV
274
    };
×
275

UNCOV
276
    window.addEventListener('keydown', handleKeyDown);
×
UNCOV
277
    return () => window.removeEventListener('keydown', handleKeyDown);
×
UNCOV
278
  }, [handleSave, handleNewNote, handleRefresh]);
×
279

280
  // Unsaved changes warning (US-015)
UNCOV
281
  useEffect(() => {
×
UNCOV
282
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
×
UNCOV
283
      if (hasUnsavedChanges) {
×
UNCOV
284
        e.preventDefault();
×
UNCOV
285
        e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
×
UNCOV
286
        return 'You have unsaved changes. Are you sure you want to leave?';
×
UNCOV
287
      }
×
UNCOV
288
    };
×
289

UNCOV
290
    window.addEventListener('beforeunload', handleBeforeUnload);
×
UNCOV
291
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
×
UNCOV
292
  }, [hasUnsavedChanges]);
×
293

UNCOV
294
  return (
×
UNCOV
295
    <>
×
UNCOV
296
      <TopNavigationBar hasUnsavedChanges={hasUnsavedChanges} onLogout={handleLogout} />
×
UNCOV
297
      <div className={styles.panels}>
×
UNCOV
298
        <LeftPanel
×
UNCOV
299
          notes={notes}
×
UNCOV
300
          selectedNoteId={selectedNoteId}
×
UNCOV
301
          onNoteSelect={handleNoteSelect}
×
UNCOV
302
          onNewNote={handleNewNote}
×
UNCOV
303
          onDeleteNote={handleDeleteNote}
×
UNCOV
304
          onRenameNote={handleRenameNote}
×
UNCOV
305
        />
×
UNCOV
306
        <MiddlePanel
×
UNCOV
307
          note={selectedNote}
×
UNCOV
308
          content={content}
×
UNCOV
309
          saveStatus={saveStatus}
×
UNCOV
310
          selectedLine={currentLine}
×
UNCOV
311
          onContentChange={handleContentChange}
×
UNCOV
312
          onSave={handleSave}
×
NEW
313
          onEditorReady={handleEditorReady}
×
UNCOV
314
        />
×
UNCOV
315
        <RightPanel
×
UNCOV
316
          headers={headers}
×
UNCOV
317
          currentLine={currentLine}
×
UNCOV
318
          onHeaderClick={handleHeaderClick}
×
UNCOV
319
        />
×
UNCOV
320
      </div>
×
UNCOV
321
      <Footer />
×
322

323
      {/* TODO: Add confirmation dialog back when needed */}
UNCOV
324
    </>
×
325
  );
UNCOV
326
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc