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

jcubic / 10xDevs / 19946653658

04 Dec 2025 10:56PM UTC coverage: 62.673%. Remained the same
19946653658

push

github

jcubic
10xDev Project

303 of 501 branches covered (60.48%)

Branch coverage included in aggregate %.

555 of 864 new or added lines in 49 files covered. (64.24%)

4 existing lines in 1 file now uncovered.

555 of 868 relevant lines covered (63.94%)

214.35 hits per line

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

85.0
/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 { 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

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

110
const NotesContext = createContext<NotesContextValue | undefined>(undefined);
36✔
111

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

148
/**
149
 * Props for NotesProvider component.
150
 *
151
 * @interface NotesProviderProps
152
 * @property {ReactNode} children - Child components to wrap with context
153
 * @property {NoteTreeNode[]} [initialNotes=[]] - Initial notes array (from server)
154
 * @property {number | null} [initialSelectedNoteId=null] - Initial selected note ID
155
 */
156
interface NotesProviderProps {
157
  children: ReactNode;
158
  initialNotes?: NoteTreeNode[];
159
  initialSelectedNoteId?: number | null;
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({
36✔
217
  children,
218
  initialNotes = [],
×
219
  initialSelectedNoteId = null
×
220
}: NotesProviderProps) {
221
  const pathname = usePathname();
1,048✔
222
  const router = useRouter();
1,048✔
223

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

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

244
  const markNoteDirty = updateDirtyFlag;
1,048✔
245

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

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

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

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

274
  // Sync URL to state on URL changes
275
  useEffect(() => {
1,048✔
276
    const noteMatch = pathname.match(/\/note\/(\d+)/);
52✔
277
    const urlNoteId = noteMatch ? parseInt(noteMatch[1], 10) : null;
52✔
278

279
    if (urlNoteId !== null) {
52✔
280
      updateSelection(urlNoteId);
20✔
281
    }
282
  }, [pathname, updateSelection]);
283

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

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

NEW
306
  return <NotesContext.Provider value={value}>{children}</NotesContext.Provider>;
×
307
}
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

© 2025 Coveralls, Inc