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

jcubic / 10xDevs / 18621254025

18 Oct 2025 09:32PM UTC coverage: 19.473% (+0.2%) from 19.299%
18621254025

push

github

jcubic
replace History API with Next.js routes system

77 of 109 branches covered (70.64%)

Branch coverage included in aggregate %.

0 of 33 new or added lines in 5 files covered. (0.0%)

118 existing lines in 9 files now uncovered.

462 of 2659 relevant lines covered (17.37%)

2.69 hits per line

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

0.44
/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
×
34
  } = useNotesContext();
×
35

36
  const editorRef = useRef<import('@/types/editor').EditorRef | null>(null);
×
37
  const [welcomeContent, setWelcomeContent] = useState<string>('');
×
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...');
×
50
      }
×
51
    };
×
52

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
62
  useEffect(() => {
×
NEW
63
    const urlParams = new URLSearchParams(window.location.search);
×
NEW
64
    const lineParam = urlParams.get('line');
×
65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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