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

SNApp-notes / web / 19855984353

02 Dec 2025 10:48AM UTC coverage: 86.761% (+0.04%) from 86.718%
19855984353

push

github

jcubic
update save confirmation

609 of 730 branches covered (83.42%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

13 existing lines in 3 files now uncovered.

1108 of 1249 relevant lines covered (88.71%)

2276.72 hits per line

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

84.13
/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 {
75
  createContext,
76
  useContext,
77
  useEffect,
78
  useCallback,
79
  useMemo,
80
  type ReactNode
81
} from 'react';
82
import { useParams, usePathname, useRouter } from 'next/navigation';
83
import type { NoteTreeNode } from '@/types/tree';
84
import type { SaveStatus } from '@/types/notes';
85
import { useNodeSelection } from '@/hooks/useNodeSelection';
86
import { selectNode } from '@/lib/utils';
87

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

120
const NotesContext = createContext<NotesContextValue | undefined>(undefined);
92✔
121

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

158
/**
159
 * Props for NotesProvider component.
160
 *
161
 * @interface NotesProviderProps
162
 * @property {ReactNode} children - Child components to wrap with context
163
 * @property {NoteTreeNode[]} [initialNotes=[]] - Initial notes array (from server)
164
 * @property {number | null} [initialSelectedNoteId=null] - Initial selected note ID
165
 */
166
interface NotesProviderProps {
167
  children: ReactNode;
168
  initialNotes?: NoteTreeNode[];
169
}
170

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

230
  // list of notes that with marked selected node if it exists in URL
231
  const initSelectedNodeId = useMemo(() => parseId(params), [params]);
3,028✔
232
  const updatedNotes = useMemo(
3,028✔
233
    () => selectNode(initialNotes, initSelectedNodeId),
726✔
234
    [initialNotes, initSelectedNodeId]
235
  );
236

237
  // Use the hook that manages all the state
238
  const {
239
    notes,
240
    selectedNoteId,
241
    saveStatus,
242
    setNotes,
243
    setSaveStatus,
244
    updateSelection,
245
    updateDirtyFlag,
246
    updateNoteContent,
247
    updateNoteName,
248
    updateNoteTimestamp
249
  } = useNodeSelection(updatedNotes, initSelectedNodeId);
3,028✔
250

251
  // Sync notes state when initialNotes prop changes (e.g., after redirect)
252
  useEffect(() => {
3,028✔
253
    if (initialNotes.length > 0 && notes.length === 0) {
100!
UNCOV
254
      setNotes(initialNotes);
×
255
    }
256
  }, [initialNotes, notes.length, setNotes]);
257

258
  const markNoteDirty = updateDirtyFlag;
3,028✔
259

260
  const getSelectedNote = useCallback((): NoteTreeNode | null => {
3,028✔
261
    if (!selectedNoteId) return null;
3,414✔
262
    return notes.find((note) => note.id === selectedNoteId) || null;
3,230!
263
  }, [notes, selectedNoteId]);
264

265
  const getNote = useCallback(
3,028✔
266
    (noteId: number): NoteTreeNode | null => {
267
      return notes.find((note) => note.id === noteId) || null;
2,718!
268
    },
269
    [notes]
270
  );
271

272
  // Note selection with Next.js router
273
  const selectNote = useCallback(
3,028✔
274
    (noteId: number | null) => {
275
      // Update state immediately
276
      updateSelection(noteId);
46✔
277

278
      // Navigate using Next.js router
279
      if (noteId === null) {
46!
UNCOV
280
        router.push('/');
×
281
      } else {
282
        router.push(`/note/${noteId}`);
46✔
283
      }
284
    },
285
    [updateSelection, router]
286
  );
287

288
  // Sync URL to state on URL changes
289
  useEffect(() => {
3,028✔
290
    const urlNoteId = parseId(params);
143✔
291

292
    if (urlNoteId !== null) {
143✔
293
      updateSelection(urlNoteId);
61✔
294
    }
295
  }, [params, updateSelection]);
296

297
  // Auto-select first note when at root with notes available
298
  useEffect(() => {
3,028✔
299
    if (pathname === '/' && notes.length > 0 && !selectedNoteId) {
1,214✔
300
      const firstNote = notes[0];
54✔
301
      router.push(`/note/${firstNote.id}`);
54✔
302
    }
303
  }, [pathname, notes, selectedNoteId, router]);
304

305
  const value: NotesContextValue = {
3,028✔
306
    notes,
307
    selectedNoteId,
308
    saveStatus,
309
    setNotes,
310
    setSaveStatus,
311
    updateNoteContent,
312
    updateNoteName,
313
    markNoteDirty,
314
    updateNoteTimestamp,
315
    getSelectedNote,
316
    getNote,
317
    selectNote
318
  };
319

UNCOV
320
  return <NotesContext.Provider value={value}>{children}</NotesContext.Provider>;
×
321
}
322

323
function parseId(params: ReturnType<typeof useParams>) {
324
  return params?.id ? parseInt(params.id as string, 10) : null;
951✔
325
}
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