diff --git a/src/hooks/useNotes.test.ts b/src/hooks/useNotes.test.ts new file mode 100644 index 0000000..20c5a73 --- /dev/null +++ b/src/hooks/useNotes.test.ts @@ -0,0 +1,103 @@ +import { act, renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useNotes } from './useNotes' +import { loadNotes } from '../lib/notesStorage' +import type { Note } from '../types' + +describe('useNotes', () => { + beforeEach(() => { + localStorage.clear() + vi.restoreAllMocks() + }) + + it('loads existing notes from storage on mount', () => { + localStorage.setItem( + 'notes', + JSON.stringify([ + { id: 1, title: 'A', content: 'a' }, + { id: 2, title: 'B', content: 'b' }, + ]), + ) + + const { result } = renderHook(() => useNotes()) + + expect(result.current.notes).toEqual([ + { id: 1, title: 'A', content: 'a' }, + { id: 2, title: 'B', content: 'b' }, + ]) + }) + + it('adds a note and persists it', () => { + vi.spyOn(Date, 'now').mockReturnValue(1234) + + const { result } = renderHook(() => useNotes()) + + act(() => { + result.current.addNote() + }) + + expect(result.current.notes).toEqual([{ id: 1234, title: '', content: '' }]) + expect(loadNotes()).toEqual([{ id: 1234, title: '', content: '' }]) + }) + + it('updates a note by id and persists it', () => { + const initial: Note[] = [{ id: 1, title: 'Old', content: 'old' }] + localStorage.setItem('notes', JSON.stringify(initial)) + + const { result } = renderHook(() => useNotes()) + + act(() => { + result.current.updateNote(1, { title: 'New', content: 'new' }) + }) + + expect(result.current.notes).toEqual([{ id: 1, title: 'New', content: 'new' }]) + expect(loadNotes()[0]).toMatchObject({ id: 1, title: 'New', content: 'new' }) + }) + + it('ignores updates for unknown ids', () => { + const initial: Note[] = [{ id: 1, title: 'A', content: 'a' }] + localStorage.setItem('notes', JSON.stringify(initial)) + + const { result } = renderHook(() => useNotes()) + + act(() => { + result.current.updateNote(999, { title: 'X' }) + }) + + expect(result.current.notes).toEqual(initial) + }) + + it('deletes a note by id and persists it', () => { + localStorage.setItem( + 'notes', + JSON.stringify([ + { id: 1, title: 'A', content: 'a' }, + { id: 2, title: 'B', content: 'b' }, + ]), + ) + + const { result } = renderHook(() => useNotes()) + + act(() => { + result.current.deleteNote(1) + }) + + expect(result.current.notes).toEqual([{ id: 2, title: 'B', content: 'b' }]) + expect(loadNotes()).toEqual([{ id: 2, title: 'B', content: 'b' }]) + }) + + it('returns notes sorted alphabetically, ignoring case', () => { + localStorage.setItem( + 'notes', + JSON.stringify([ + { id: 1, title: 'zebra', content: '' }, + { id: 2, title: 'Banane', content: '' }, + { id: 3, title: 'abricot', content: '' }, + ]), + ) + + const { result } = renderHook(() => useNotes()) + + expect(result.current.notes.map((n) => n.title)).toEqual(['abricot', 'Banane', 'zebra']) + }) +}) diff --git a/src/hooks/useNotes.ts b/src/hooks/useNotes.ts new file mode 100644 index 0000000..6630831 --- /dev/null +++ b/src/hooks/useNotes.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import type { Note } from '../types' +import { loadNotes, saveNotes } from '../lib/notesStorage' + +export function useNotes() { + const [notes, setNotes] = useState(() => loadNotes()) + + useEffect(() => { + saveNotes(notes) + }, [notes]) + + const addNote = useCallback(() => { + const note: Note = { id: Date.now(), title: '', content: '' } + setNotes((current) => [...current, note]) + return note + }, []) + + const updateNote = useCallback((id: number, patch: Partial>) => { + setNotes((current) => current.map((note) => (note.id === id ? { ...note, ...patch } : note))) + }, []) + + const deleteNote = useCallback((id: number) => { + setNotes((current) => current.filter((note) => note.id !== id)) + }, []) + + const sortedNotes = useMemo( + () => + [...notes].sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })), + [notes], + ) + + return { notes: sortedNotes, addNote, updateNote, deleteNote } +}