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

jcubic / 10xDevs / 18600576710

17 Oct 2025 05:47PM UTC coverage: 20.807% (-0.1%) from 20.937%
18600576710

push

github

jcubic
run prettier on example note Markdown file

64 of 92 branches covered (69.57%)

Branch coverage included in aggregate %.

426 of 2263 relevant lines covered (18.82%)

2.34 hits per line

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

0.48
/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
  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

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

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

54
  const selectedNote = getSelectedNote();
×
55
  // Use the same logic as MiddlePanel to determine the actual content being displayed
56
  const content =
×
57
    selectedNote?.data?.content === null
×
58
      ? welcomeContent
×
59
      : selectedNote?.data?.content || '';
×
60
  const hasUnsavedChanges = selectedNote?.data?.dirty || false;
×
61

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

65
  const handleNoteSelect = useCallback(
×
66
    (noteId: number) => {
×
67
      // Navigate to the selected note
68
      router.push(`/note/${noteId}`);
×
69
    },
×
70
    [router]
×
71
  );
×
72

73
  const handleNewNote = useCallback(async () => {
×
74
    try {
×
75
      const newNote = await createNote('New Note');
×
76

77
      // Add the new note to our local state
78
      const newTreeNode: NoteTreeNode = {
×
79
        id: newNote.id,
×
80
        name: newNote.name,
×
81
        data: {
×
82
          content: newNote.content || '',
×
83
          dirty: false
×
84
        }
×
85
      };
×
86

87
      setNotes((prevNotes: NoteTreeNode[]) => [newTreeNode, ...prevNotes]);
×
88
      router.push(`/note/${newNote.id}`);
×
89
    } catch (error) {
×
90
      console.error('Failed to create note:', error);
×
91
    }
×
92
  }, [setNotes, router]);
×
93

94
  const handleContentChange = (newContent: string) => {
×
95
    if (selectedNoteId) {
×
96
      updateNoteContent(selectedNoteId, newContent);
×
97
    }
×
98
  };
×
99

100
  const handleSave = useCallback(async () => {
×
101
    if (!selectedNote) return;
×
102

103
    try {
×
104
      setSaveStatus('saving');
×
105
      await updateNote(selectedNote.id, { content });
×
106
      setSaveStatus('saved');
×
107
      markNoteDirty(selectedNote.id, false);
×
108

109
      // Reset status after 2 seconds
110
      setTimeout(() => setSaveStatus('idle'), 2000);
×
111
    } catch (error) {
×
112
      setSaveStatus('error');
×
113
      console.error('Failed to save note:', error);
×
114
    }
×
115
  }, [selectedNote, content, setSaveStatus, markNoteDirty]);
×
116

117
  const handleRefresh = useCallback(async () => {
×
118
    if (!selectedNote) return;
×
119

120
    try {
×
121
      setSaveStatus('saving');
×
122
      const freshNote = await updateNote(selectedNote.id, {}); // Fetch without changes
×
123
      updateNoteContent(selectedNote.id, freshNote.content || '');
×
124
      markNoteDirty(selectedNote.id, false);
×
125
      setSaveStatus('idle');
×
126
    } catch (error) {
×
127
      setSaveStatus('error');
×
128
      console.error('Failed to refresh note:', error);
×
129
    }
×
130
  }, [selectedNote, setSaveStatus, updateNoteContent, markNoteDirty]);
×
131

132
  const handleDeleteNote = useCallback(
×
133
    async (noteId: number) => {
×
134
      try {
×
135
        await deleteNote(noteId);
×
136

137
        // Remove from local state
138
        setNotes((prevNotes: NoteTreeNode[]) =>
×
139
          prevNotes.filter((note: NoteTreeNode) => note.id !== noteId)
×
140
        );
×
141

142
        // If we deleted the currently selected note, navigate home
143
        if (selectedNoteId === noteId) {
×
144
          router.push('/');
×
145
        }
×
146
      } catch (error) {
×
147
        console.error('Failed to delete note:', error);
×
148
      }
×
149
    },
×
150
    [setNotes, selectedNoteId, router]
×
151
  );
×
152

153
  const handleRenameNote = useCallback(
×
154
    async (noteId: number, newName: string) => {
×
155
      try {
×
156
        const updatedNote = await updateNote(noteId, { name: newName });
×
157
        updateNoteName(noteId, updatedNote.name);
×
158
      } catch (error) {
×
159
        console.error('Failed to rename note:', error);
×
160
        throw error; // Re-throw to let the component handle the error
×
161
      }
×
162
    },
×
163
    [updateNoteName]
×
164
  );
×
165

166
  const handleLogout = () => {
×
167
    // TODO: Implement logout logic
168
    console.log('Logout clicked');
×
169
  };
×
170

171
  const handleHeaderClick = (line: number) => {
×
172
    // Update URL with line number
173
    if (selectedNoteId) {
×
174
      router.push(`/note/${selectedNoteId}?line=${line}`, { scroll: false });
×
175
    }
×
176

177
    // Scroll to line in CodeMirror editor
178
    if (editorRef.current) {
×
179
      editorRef.current.scrollToLine(line);
×
180
    }
×
181
  };
×
182

183
  // Keyboard shortcuts
184
  useEffect(() => {
×
185
    const handleKeyDown = (e: KeyboardEvent) => {
×
186
      if (e.ctrlKey || e.metaKey) {
×
187
        switch (e.key) {
×
188
          case 's':
×
189
            e.preventDefault();
×
190
            handleSave();
×
191
            break;
×
192
          case 'n':
×
193
            e.preventDefault();
×
194
            handleNewNote();
×
195
            break;
×
196
          case 'r':
×
197
            // Only trigger refresh when focused in editor
198
            if (document.activeElement?.closest('[data-editor]')) {
×
199
              e.preventDefault();
×
200
              handleRefresh();
×
201
            }
×
202
            break;
×
203
        }
×
204
      }
×
205
    };
×
206

207
    window.addEventListener('keydown', handleKeyDown);
×
208
    return () => window.removeEventListener('keydown', handleKeyDown);
×
209
  }, [handleSave, handleNewNote, handleRefresh]);
×
210

211
  // Unsaved changes warning (US-015)
212
  useEffect(() => {
×
213
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
×
214
      if (hasUnsavedChanges) {
×
215
        e.preventDefault();
×
216
        e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
×
217
        return 'You have unsaved changes. Are you sure you want to leave?';
×
218
      }
×
219
    };
×
220

221
    window.addEventListener('beforeunload', handleBeforeUnload);
×
222
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
×
223
  }, [hasUnsavedChanges]);
×
224

225
  return (
×
226
    <>
×
227
      <TopNavigationBar hasUnsavedChanges={hasUnsavedChanges} onLogout={handleLogout} />
×
228
      <div className={styles.panels}>
×
229
        <LeftPanel
×
230
          notes={notes}
×
231
          selectedNoteId={selectedNoteId}
×
232
          onNoteSelect={handleNoteSelect}
×
233
          onNewNote={handleNewNote}
×
234
          onDeleteNote={handleDeleteNote}
×
235
          onRenameNote={handleRenameNote}
×
236
        />
×
237
        <MiddlePanel
×
238
          note={selectedNote}
×
239
          content={content}
×
240
          saveStatus={saveStatus}
×
241
          selectedLine={lineNumber}
×
242
          onContentChange={handleContentChange}
×
243
          onSave={handleSave}
×
244
          onEditorReady={(editor) => {
×
245
            editorRef.current = editor;
×
246
          }}
×
247
        />
×
248
        <RightPanel
×
249
          headers={headers}
×
250
          currentLine={lineNumber}
×
251
          onHeaderClick={handleHeaderClick}
×
252
        />
×
253
      </div>
×
254
      <Footer />
×
255

256
      {/* TODO: Add confirmation dialog back when needed */}
257
    </>
×
258
  );
259
}
×
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