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

jcubic / 10xDevs / 18858339315

27 Oct 2025 10:58PM UTC coverage: 41.94% (+1.2%) from 40.715%
18858339315

push

github

jcubic
update E2E tests

206 of 506 branches covered (40.71%)

Branch coverage included in aggregate %.

408 of 958 relevant lines covered (42.59%)

42.24 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 [currentLine, setCurrentLine] = useState<number | undefined>(lineNumber);
×
37

38
  // Sync currentLine with lineNumber prop changes
39
  useEffect(() => {
×
40
    setCurrentLine(lineNumber);
×
41
  }, [lineNumber]);
42

43
  // Monitor URL changes for line parameter
44
  useEffect(() => {
×
45
    const urlParams = new URLSearchParams(window.location.search);
×
46
    const lineParam = urlParams.get('line');
×
47

48
    if (lineParam) {
×
49
      const parsedLine = parseInt(lineParam, 10);
×
50
      if (!isNaN(parsedLine)) {
×
51
        setCurrentLine(parsedLine);
×
52
        return;
×
53
      }
54
    }
55

56
    setCurrentLine(undefined);
×
57
  }, [lineNumber]); // Re-run when lineNumber prop changes
58

59
  const selectedNote = getSelectedNote();
×
60
  // Content is now populated server-side, no null values expected
61
  const content = selectedNote?.data?.content || '';
×
62
  const hasUnsavedChanges = selectedNote?.data?.dirty || false;
×
63

64
  // Extract headers from current content (the actual content being displayed)
65
  const headers = useMemo(() => extractHeaders(content), [content]);
×
66

67
  // Memoize editor ready handler to prevent re-renders
68
  const handleEditorReady = useCallback((editor: import('@/types/editor').EditorRef) => {
×
69
    editorRef.current = editor;
×
70
  }, []);
71

72
  const handleNoteSelect = useCallback(
×
73
    (noteId: number) => {
74
      // Navigate to the selected note
75
      router.push(`/note/${noteId}`);
×
76
    },
77
    [router]
78
  );
79

80
  const handleNewNote = useCallback(async () => {
×
81
    try {
×
82
      const newNote = await createNote('New Note');
×
83

84
      // Add the new note to our local state
85
      const newTreeNode: NoteTreeNode = {
×
86
        id: newNote.id,
87
        name: newNote.name,
88
        selected: false,
89
        data: {
90
          content: newNote.content || '',
×
91
          dirty: false
92
        }
93
      };
94

95
      setNotes((prevNotes: NoteTreeNode[]) => [newTreeNode, ...prevNotes]);
×
96
      router.push(`/note/${newNote.id}`);
×
97
    } catch (error) {
98
      console.error('Failed to create note:', error);
×
99
    }
100
  }, [setNotes, router]);
101

102
  const handleContentChange = useCallback(
×
103
    (newContent: string) => {
104
      if (selectedNoteId) {
×
105
        // Update local state immediately for responsiveness
106
        updateNoteContent(selectedNoteId, newContent);
×
107
        // Manual save only - no automatic background saving
108
      }
109
    },
110
    [selectedNoteId, updateNoteContent]
111
  );
112

113
  const handleSave = useCallback(async () => {
×
114
    if (!selectedNote) return;
×
115

116
    try {
×
117
      setSaveStatus('saving');
×
118
      await updateNote(selectedNote.id, { content });
×
119
      setSaveStatus('saved');
×
120
      markNoteDirty(selectedNote.id, false);
×
121

122
      // Reset status after 2 seconds
123
      setTimeout(() => setSaveStatus('idle'), 2000);
×
124
    } catch (error) {
125
      setSaveStatus('error');
×
126
      console.error('Failed to save note:', error);
×
127
    }
128
  }, [selectedNote, content, setSaveStatus, markNoteDirty]);
129

130
  const handleRefresh = useCallback(async () => {
×
131
    if (!selectedNote) return;
×
132

133
    try {
×
134
      setSaveStatus('saving');
×
135
      const freshNote = await updateNote(selectedNote.id, {}); // Fetch without changes
×
136
      updateNoteContent(selectedNote.id, freshNote.content || '');
×
137
      markNoteDirty(selectedNote.id, false);
×
138
      setSaveStatus('idle');
×
139
    } catch (error) {
140
      setSaveStatus('error');
×
141
      console.error('Failed to refresh note:', error);
×
142
    }
143
  }, [selectedNote, setSaveStatus, updateNoteContent, markNoteDirty]);
144

145
  const handleDeleteNote = useCallback(
×
146
    async (noteId: number) => {
147
      try {
×
148
        await deleteNote(noteId);
×
149

150
        // Remove from local state
151
        setNotes((prevNotes: NoteTreeNode[]) =>
×
152
          prevNotes.filter((note: NoteTreeNode) => note.id !== noteId)
×
153
        );
154

155
        // If we deleted the currently selected note, navigate home
156
        if (selectedNoteId === noteId) {
×
157
          router.push('/');
×
158
        }
159
      } catch (error) {
160
        console.error('Failed to delete note:', error);
×
161
      }
162
    },
163
    [setNotes, selectedNoteId, router]
164
  );
165

166
  const handleRenameNote = useCallback(
×
167
    async (noteId: number, newName: string) => {
168
      try {
×
169
        const updatedNote = await updateNote(noteId, { name: newName });
×
170
        updateNoteName(noteId, updatedNote.name);
×
171
      } catch (error) {
172
        console.error('Failed to rename note:', error);
×
173
        throw error; // Re-throw to let the component handle the error
×
174
      }
175
    },
176
    [updateNoteName]
177
  );
178

179
  const handleHeaderClick = useCallback((line: number) => {
×
180
    // Update current line state immediately for visual feedback
181
    setCurrentLine(line);
×
182

183
    if (editorRef.current) {
×
184
      editorRef.current.scrollToLine(line);
×
185
    }
186

187
    // TODO: Use Next.js router to update URL with line parameter
188
    // For now, just update the line state without URL manipulation
189
  }, []);
190

191
  const handleLogout = () => {
×
192
    // TODO: Implement logout logic
193
    console.log('Logout clicked');
×
194
  };
195

196
  // Keyboard shortcuts
197
  useEffect(() => {
×
198
    const handleKeyDown = (e: KeyboardEvent) => {
×
199
      if (e.ctrlKey || e.metaKey) {
×
200
        switch (e.key) {
×
201
          case 's':
202
            e.preventDefault();
×
203
            handleSave();
×
204
            break;
×
205
          case 'n':
206
            e.preventDefault();
×
207
            handleNewNote();
×
208
            break;
×
209
          case 'r':
210
            // Only trigger refresh when focused in editor
211
            if (document.activeElement?.closest('[data-editor]')) {
×
212
              e.preventDefault();
×
213
              handleRefresh();
×
214
            }
215
            break;
×
216
        }
217
      }
218
    };
219

220
    window.addEventListener('keydown', handleKeyDown);
×
221
    return () => window.removeEventListener('keydown', handleKeyDown);
×
222
  }, [handleSave, handleNewNote, handleRefresh]);
223

224
  // Unsaved changes warning (US-015)
225
  useEffect(() => {
×
226
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
×
227
      if (hasUnsavedChanges) {
×
228
        e.preventDefault();
×
229
        e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
×
230
        return 'You have unsaved changes. Are you sure you want to leave?';
×
231
      }
232
    };
233

234
    window.addEventListener('beforeunload', handleBeforeUnload);
×
235
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
×
236
  }, [hasUnsavedChanges]);
237

238
  return (
×
239
    <>
240
      <TopNavigationBar hasUnsavedChanges={hasUnsavedChanges} onLogout={handleLogout} />
241
      <div className={styles.panels}>
242
        <LeftPanel
243
          notes={notes}
244
          onNoteSelect={handleNoteSelect}
245
          onNewNote={handleNewNote}
246
          onDeleteNote={handleDeleteNote}
247
          onRenameNote={handleRenameNote}
248
        />
249
        <MiddlePanel
250
          note={selectedNote}
251
          content={content}
252
          saveStatus={saveStatus}
253
          selectedLine={currentLine}
254
          onContentChange={handleContentChange}
255
          onSave={handleSave}
256
          onEditorReady={handleEditorReady}
257
        />
258
        <RightPanel
259
          headers={headers}
260
          currentLine={currentLine}
261
          onHeaderClick={handleHeaderClick}
262
        />
263
      </div>
264

265
      {/* TODO: Add confirmation dialog back when needed */}
266
    </>
267
  );
268
}
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