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

SNApp-notes / web / 19475216851

18 Nov 2025 05:27PM UTC coverage: 68.962% (+0.4%) from 68.605%
19475216851

push

github

jcubic
add unit test for GitHub sign in button

458 of 681 branches covered (67.25%)

Branch coverage included in aggregate %.

824 of 1178 relevant lines covered (69.95%)

438.08 hits per line

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

83.87
/src/components/notes/NotesContext.tsx
1
/**
2
 * @module components/notes/NotesContext
3
 * @description React Context for managing global notes state with URL synchronization.
4
 * Provides notes tree, selection state, save status, and CRUD operations to all child components.
5
 *
6
 * @dependencies
7
 * - `next/navigation` - usePathname and useRouter for URL-based routing
8
 * - `@/hooks/useNodeSelection` - Core state management hook for notes
9
 * - `@/types/tree` - NoteTreeNode type definition
10
 * - `@/types/notes` - SaveStatus type definition
11
 *
12
 * @remarks
13
 * **Features:**
14
 * - Global notes tree state (flat array of notes with selection flags)
15
 * - Selected note tracking with URL synchronization
16
 * - Save status management (saving, saved, error, unsaved)
17
 * - Note CRUD operations (update content, update name, mark dirty)
18
 * - Auto-select first note when navigating to root
19
 * - Next.js router integration for note navigation
20
 *
21
 * **URL Synchronization:**
22
 * - `/note/:id` routes automatically sync to context state
23
 * - `selectNote(id)` updates both state and URL
24
 * - Browser back/forward buttons work correctly
25
 * - Initial note selection from URL on mount
26
 *
27
 * **State Management:**
28
 * - Uses `useNodeSelection` hook for core logic
29
 * - Context wraps hook state for global access
30
 * - Initial state from server-side props (SSR-friendly)
31
 * - Automatically syncs when initial props change
32
 *
33
 * **Performance:**
34
 * - Memoized callbacks to prevent unnecessary re-renders
35
 * - Selective updates (only changed notes re-render)
36
 * - useCallback for all functions to stabilize references
37
 *
38
 * @example
39
 * ```tsx
40
 * import { NotesProvider, useNotesContext } from '@/components/notes/NotesContext';
41
 *
42
 * // Wrap app with provider
43
 * export default function NotesLayout({ children, initialNotes }) {
44
 *   return (
45
 *     <NotesProvider initialNotes={initialNotes}>
46
 *       {children}
47
 *     </NotesProvider>
48
 *   );
49
 * }
50
 *
51
 * // Use context in child component
52
 * function NoteEditor() {
53
 *   const {
54
 *     notes,
55
 *     selectedNoteId,
56
 *     updateNoteContent,
57
 *     saveStatus
58
 *   } = useNotesContext();
59
 *
60
 *   const selectedNote = notes.find(n => n.id === selectedNoteId);
61
 *
62
 *   return (
63
 *     <Editor
64
 *       value={selectedNote?.data?.content || ''}
65
 *       onChange={(content) => updateNoteContent(selectedNoteId!, content)}
66
 *       saveStatus={saveStatus}
67
 *     />
68
 *   );
69
 * }
70
 * ```
71
 */
72
'use client';
73

74
import { createContext, useContext, useEffect, useCallback, type ReactNode } from 'react';
75
import { useParams, usePathname, useRouter } from 'next/navigation';
76
import type { NoteTreeNode } from '@/types/tree';
77
import type { SaveStatus } from '@/types/notes';
78
import { useNodeSelection } from '@/hooks/useNodeSelection';
79
import { selectNode } from '@/lib/utils';
80

81
/**
82
 * NotesContext value interface exposing all notes state and operations.
83
 *
84
 * @interface NotesContextValue
85
 * @property {NoteTreeNode[]} notes - Array of all notes with selection and dirty flags
86
 * @property {number | null} selectedNoteId - Currently selected note ID (null if none)
87
 * @property {SaveStatus} saveStatus - Current save state ('saving' | 'saved' | 'error' | 'unsaved')
88
 * @property {(notes: NoteTreeNode[] | ((prev: NoteTreeNode[]) => NoteTreeNode[])) => void} setNotes - Update entire notes array
89
 * @property {(status: SaveStatus) => void} setSaveStatus - Update save status
90
 * @property {(noteId: number, content: string) => void} updateNoteContent - Update note content (marks as dirty)
91
 * @property {(noteId: number, name: string) => void} updateNoteName - Update note name (marks as dirty)
92
 * @property {(noteId: number, dirty: boolean) => void} markNoteDirty - Set note's dirty flag
93
 * @property {() => NoteTreeNode | null} getSelectedNote - Get currently selected note object
94
 * @property {(noteId: number) => NoteTreeNode | null} getNote - Get note by ID
95
 * @property {(noteId: number | null) => void} selectNote - Select note and navigate to URL
96
 */
97
interface NotesContextValue {
98
  notes: NoteTreeNode[];
99
  selectedNoteId: number | null;
100
  saveStatus: SaveStatus;
101
  setNotes: (notes: NoteTreeNode[] | ((prev: NoteTreeNode[]) => NoteTreeNode[])) => void;
102
  setSaveStatus: (status: SaveStatus) => void;
103
  updateNoteContent: (noteId: number, content: string) => void;
104
  updateNoteName: (noteId: number, name: string) => void;
105
  markNoteDirty: (noteId: number, dirty: boolean) => void;
106
  getSelectedNote: () => NoteTreeNode | null;
107
  getNote: (noteId: number) => NoteTreeNode | null;
108
  selectNote: (noteId: number | null) => void;
109
}
110

111
const NotesContext = createContext<NotesContextValue | undefined>(undefined);
60✔
112

113
/**
114
 * Hook to access NotesContext value in child components.
115
 *
116
 * @hook
117
 * @returns {NotesContextValue} Notes context value with state and operations
118
 * @throws {Error} If used outside of NotesProvider
119
 *
120
 * @remarks
121
 * Must be used within a component wrapped by `<NotesProvider>`.
122
 * Throws error if context is undefined (not within provider).
123
 *
124
 * @example
125
 * ```tsx
126
 * function NotesList() {
127
 *   const { notes, selectNote } = useNotesContext();
128
 *
129
 *   return (
130
 *     <ul>
131
 *       {notes.map(note => (
132
 *         <li key={note.id} onClick={() => selectNote(note.id)}>
133
 *           {note.name}
134
 *         </li>
135
 *       ))}
136
 *     </ul>
137
 *   );
138
 * }
139
 * ```
140
 */
141
export function useNotesContext() {
60✔
142
  const context = useContext(NotesContext);
3,990✔
143
  if (context === undefined) {
3,990!
144
    throw new Error('useNotesContext must be used within a NotesProvider');
×
145
  }
146
  return context;
3,990✔
147
}
148

149
/**
150
 * Props for NotesProvider component.
151
 *
152
 * @interface NotesProviderProps
153
 * @property {ReactNode} children - Child components to wrap with context
154
 * @property {NoteTreeNode[]} [initialNotes=[]] - Initial notes array (from server)
155
 * @property {number | null} [initialSelectedNoteId=null] - Initial selected note ID
156
 */
157
interface NotesProviderProps {
158
  children: ReactNode;
159
  initialNotes?: NoteTreeNode[];
160
}
161

162
/**
163
 * Notes context provider with URL synchronization and auto-selection.
164
 *
165
 * @component
166
 * @param {NotesProviderProps} props - Provider configuration
167
 * @returns {JSX.Element} Provider wrapping children
168
 *
169
 * @remarks
170
 * **Initialization:**
171
 * - Accepts `initialNotes` from server-side props (SSR)
172
 * - Accepts `initialSelectedNoteId` to pre-select a note
173
 * - Syncs initial state when props change (e.g., after navigation)
174
 *
175
 * **URL Synchronization:**
176
 * - Watches pathname for `/note/:id` pattern
177
 * - Updates selection state when URL changes (e.g., browser back/forward)
178
 * - `selectNote()` updates both state and URL via Next.js router
179
 *
180
 * **Auto-selection:**
181
 * - If at root (`/`) with notes available and none selected, auto-selects first note
182
 * - Automatically navigates to `/note/:id` after auto-selection
183
 * - Prevents empty state when notes exist
184
 *
185
 * **State Management:**
186
 * - Uses `useNodeSelection` hook for core state logic
187
 * - Exposes state and operations via context value
188
 * - Memoized callbacks to prevent unnecessary re-renders
189
 *
190
 * **Operations:**
191
 * - `updateNoteContent`: Updates content and marks as dirty
192
 * - `updateNoteName`: Updates name and marks as dirty
193
 * - `markNoteDirty`: Sets dirty flag (true = unsaved changes)
194
 * - `selectNote`: Updates selection and navigates to URL
195
 * - `getSelectedNote`: Returns currently selected note object
196
 * - `getNote`: Returns note by ID
197
 *
198
 * @example
199
 * ```tsx
200
 * // In layout component
201
 * export default async function NotesLayout({ children }) {
202
 *   const notes = await getNotes();
203
 *   const selectedId = getSelectedNoteIdFromUrl();
204
 *
205
 *   return (
206
 *     <NotesProvider
207
 *       initialNotes={notes}
208
 *       initialSelectedNoteId={selectedId}
209
 *     >
210
 *       {children}
211
 *     </NotesProvider>
212
 *   );
213
 * }
214
 * ```
215
 */
216
export function NotesProvider({ children, initialNotes = [] }: NotesProviderProps) {
60!
217
  const params = useParams();
1,398✔
218
  const pathname = usePathname();
1,398✔
219
  const router = useRouter();
1,398✔
220

221
  // list of notes that with marked selected node if it exists in URL
222
  const initSelectedNodeId = parseId(params);
1,398✔
223
  const updatedNotes = selectNode(initialNotes, initSelectedNodeId);
1,398✔
224

225
  // Use the hook that manages all the state
226
  const {
227
    notes,
228
    selectedNoteId,
229
    saveStatus,
230
    setNotes,
231
    setSaveStatus,
232
    updateSelection,
233
    updateDirtyFlag,
234
    updateNoteContent,
235
    updateNoteName
236
  } = useNodeSelection(updatedNotes, initSelectedNodeId);
1,398✔
237

238
  // Sync notes state when initialNotes prop changes (e.g., after redirect)
239
  useEffect(() => {
1,398✔
240
    if (initialNotes.length > 0 && notes.length === 0) {
62!
241
      setNotes(initialNotes);
×
242
    }
243
  }, [initialNotes, notes.length, setNotes]);
244

245
  const markNoteDirty = updateDirtyFlag;
1,398✔
246

247
  const getSelectedNote = useCallback((): NoteTreeNode | null => {
1,398✔
248
    if (!selectedNoteId) return null;
1,682✔
249
    return notes.find((note) => note.id === selectedNoteId) || null;
1,268!
250
  }, [notes, selectedNoteId]);
251

252
  const getNote = useCallback(
1,398✔
253
    (noteId: number): NoteTreeNode | null => {
254
      return notes.find((note) => note.id === noteId) || null;
1,066!
255
    },
256
    [notes]
257
  );
258

259
  // Note selection with Next.js router
260
  const selectNote = useCallback(
1,398✔
261
    (noteId: number | null) => {
262
      // Update state immediately
263
      updateSelection(noteId);
20✔
264

265
      // Navigate using Next.js router
266
      if (noteId === null) {
20!
267
        router.push('/');
×
268
      } else {
269
        router.push(`/note/${noteId}`);
20✔
270
      }
271
    },
272
    [updateSelection, router]
273
  );
274

275
  // Sync URL to state on URL changes
276
  useEffect(() => {
1,398✔
277
    const urlNoteId = parseId(params);
102✔
278

279
    if (urlNoteId !== null) {
102✔
280
      updateSelection(urlNoteId);
48✔
281
    }
282
  }, [params, updateSelection]);
283

284
  // Auto-select first note when at root with notes available
285
  useEffect(() => {
1,398✔
286
    if (pathname === '/' && notes.length > 0 && !selectedNoteId) {
481✔
287
      const firstNote = notes[0];
39✔
288
      router.push(`/note/${firstNote.id}`);
39✔
289
    }
290
  }, [pathname, notes, selectedNoteId, router]);
291

292
  const value: NotesContextValue = {
1,398✔
293
    notes,
294
    selectedNoteId,
295
    saveStatus,
296
    setNotes,
297
    setSaveStatus,
298
    updateNoteContent,
299
    updateNoteName,
300
    markNoteDirty,
301
    getSelectedNote,
302
    getNote,
303
    selectNote
304
  };
305

306
  return <NotesContext.Provider value={value}>{children}</NotesContext.Provider>;
×
307
}
308

309
function parseId(params: ReturnType<typeof useParams>): number | null {
310
  return params?.id ? parseInt(params.id as string, 10) : null;
1,500✔
311
}
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