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

jcubic / 10xDevs / 18822337453

26 Oct 2025 06:58PM UTC coverage: 25.603% (+0.6%) from 24.984%
18822337453

push

github

jcubic
update migration script to include header separators

1885 of 8871 branches covered (21.25%)

Branch coverage included in aggregate %.

884 of 1944 relevant lines covered (45.47%)

1776.26 hits per line

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

0.0
/src/components/notes/MainNotesClient.tsx
1
'use client';
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

14
import styles from './MainNotesLayout.module.css';
15

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

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

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

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

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

52
    loadWelcomeContent();
×
53
  }, []);
54

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

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

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

73
    setCurrentLine(undefined);
×
74
  }, [lineNumber]); // Re-run when lineNumber prop changes
75

76
  const selectedNote = getSelectedNote();
×
77
  // Use the same logic as MiddlePanel to determine the actual content being displayed
78
  const content =
79
    selectedNote?.data?.content === null
×
80
      ? welcomeContent
81
      : selectedNote?.data?.content || '';
×
82
  const hasUnsavedChanges = selectedNote?.data?.dirty || false;
×
83

84
  // Extract headers from current content (the actual content being displayed)
85
  const headers = useMemo(() => extractHeaders(content), [content]);
×
86

87
  // Memoize editor ready handler to prevent re-renders
88
  const handleEditorReady = useCallback((editor: import('@/types/editor').EditorRef) => {
×
89
    editorRef.current = editor;
×
90
  }, []);
91

92
  const handleNoteSelect = useCallback(
×
93
    (noteId: number) => {
94
      // Navigate to the selected note
95
      router.push(`/note/${noteId}`);
×
96
    },
97
    [router]
98
  );
99

100
  const handleNewNote = useCallback(async () => {
×
101
    try {
×
102
      const newNote = await createNote('New Note');
×
103

104
      // Add the new note to our local state
105
      const newTreeNode: NoteTreeNode = {
×
106
        id: newNote.id,
107
        name: newNote.name,
108
        selected: false,
109
        data: {
110
          content: newNote.content || '',
×
111
          dirty: false
112
        }
113
      };
114

115
      setNotes((prevNotes: NoteTreeNode[]) => [newTreeNode, ...prevNotes]);
×
116
      router.push(`/note/${newNote.id}`);
×
117
    } catch (error) {
118
      console.error('Failed to create note:', error);
×
119
    }
120
  }, [setNotes, router]);
121

122
  const handleContentChange = useCallback(
×
123
    (newContent: string) => {
124
      if (selectedNoteId) {
×
125
        // Update local state immediately for responsiveness
126
        updateNoteContent(selectedNoteId, newContent);
×
127
        // Manual save only - no automatic background saving
128
      }
129
    },
130
    [selectedNoteId, updateNoteContent]
131
  );
132

133
  const handleSave = useCallback(async () => {
×
134
    if (!selectedNote) return;
×
135

136
    try {
×
137
      setSaveStatus('saving');
×
138
      await updateNote(selectedNote.id, { content });
×
139
      setSaveStatus('saved');
×
140
      markNoteDirty(selectedNote.id, false);
×
141

142
      // Reset status after 2 seconds
143
      setTimeout(() => setSaveStatus('idle'), 2000);
×
144
    } catch (error) {
145
      setSaveStatus('error');
×
146
      console.error('Failed to save note:', error);
×
147
    }
148
  }, [selectedNote, content, setSaveStatus, markNoteDirty]);
149

150
  const handleRefresh = useCallback(async () => {
×
151
    if (!selectedNote) return;
×
152

153
    try {
×
154
      setSaveStatus('saving');
×
155
      const freshNote = await updateNote(selectedNote.id, {}); // Fetch without changes
×
156
      updateNoteContent(selectedNote.id, freshNote.content || '');
×
157
      markNoteDirty(selectedNote.id, false);
×
158
      setSaveStatus('idle');
×
159
    } catch (error) {
160
      setSaveStatus('error');
×
161
      console.error('Failed to refresh note:', error);
×
162
    }
163
  }, [selectedNote, setSaveStatus, updateNoteContent, markNoteDirty]);
164

165
  const handleDeleteNote = useCallback(
×
166
    async (noteId: number) => {
167
      try {
×
168
        await deleteNote(noteId);
×
169

170
        // Remove from local state
171
        setNotes((prevNotes: NoteTreeNode[]) =>
×
172
          prevNotes.filter((note: NoteTreeNode) => note.id !== noteId)
×
173
        );
174

175
        // If we deleted the currently selected note, navigate home
176
        if (selectedNoteId === noteId) {
×
177
          router.push('/');
×
178
        }
179
      } catch (error) {
180
        console.error('Failed to delete note:', error);
×
181
      }
182
    },
183
    [setNotes, selectedNoteId, router]
184
  );
185

186
  const handleRenameNote = useCallback(
×
187
    async (noteId: number, newName: string) => {
188
      try {
×
189
        const updatedNote = await updateNote(noteId, { name: newName });
×
190
        updateNoteName(noteId, updatedNote.name);
×
191
      } catch (error) {
192
        console.error('Failed to rename note:', error);
×
193
        throw error; // Re-throw to let the component handle the error
×
194
      }
195
    },
196
    [updateNoteName]
197
  );
198

199
  const handleHeaderClick = useCallback((line: number) => {
×
200
    // Update current line state immediately for visual feedback
201
    setCurrentLine(line);
×
202

203
    if (editorRef.current) {
×
204
      editorRef.current.scrollToLine(line);
×
205
    }
206

207
    // TODO: Use Next.js router to update URL with line parameter
208
    // For now, just update the line state without URL manipulation
209
  }, []);
210

211
  const handleLogout = () => {
×
212
    // TODO: Implement logout logic
213
    console.log('Logout clicked');
×
214
  };
215

216
  // Keyboard shortcuts
217
  useEffect(() => {
×
218
    const handleKeyDown = (e: KeyboardEvent) => {
×
219
      if (e.ctrlKey || e.metaKey) {
×
220
        switch (e.key) {
×
221
          case 's':
222
            e.preventDefault();
×
223
            handleSave();
×
224
            break;
×
225
          case 'n':
226
            e.preventDefault();
×
227
            handleNewNote();
×
228
            break;
×
229
          case 'r':
230
            // Only trigger refresh when focused in editor
231
            if (document.activeElement?.closest('[data-editor]')) {
×
232
              e.preventDefault();
×
233
              handleRefresh();
×
234
            }
235
            break;
×
236
        }
237
      }
238
    };
239

240
    window.addEventListener('keydown', handleKeyDown);
×
241
    return () => window.removeEventListener('keydown', handleKeyDown);
×
242
  }, [handleSave, handleNewNote, handleRefresh]);
243

244
  // Unsaved changes warning (US-015)
245
  useEffect(() => {
×
246
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
×
247
      if (hasUnsavedChanges) {
×
248
        e.preventDefault();
×
249
        e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
×
250
        return 'You have unsaved changes. Are you sure you want to leave?';
×
251
      }
252
    };
253

254
    window.addEventListener('beforeunload', handleBeforeUnload);
×
255
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
×
256
  }, [hasUnsavedChanges]);
257

258
  return (
×
259
    <>
260
      <TopNavigationBar hasUnsavedChanges={hasUnsavedChanges} onLogout={handleLogout} />
261
      <div className={styles.panels}>
262
        <LeftPanel
263
          notes={notes}
264
          onNoteSelect={handleNoteSelect}
265
          onNewNote={handleNewNote}
266
          onDeleteNote={handleDeleteNote}
267
          onRenameNote={handleRenameNote}
268
        />
269
        <MiddlePanel
270
          note={selectedNote}
271
          content={content}
272
          saveStatus={saveStatus}
273
          selectedLine={currentLine}
274
          onContentChange={handleContentChange}
275
          onSave={handleSave}
276
          onEditorReady={handleEditorReady}
277
        />
278
        <RightPanel
279
          headers={headers}
280
          currentLine={currentLine}
281
          onHeaderClick={handleHeaderClick}
282
        />
283
      </div>
284

285
      {/* TODO: Add confirmation dialog back when needed */}
286
    </>
287
  );
288
}
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