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

jcubic / 10xDevs / 18543522652

15 Oct 2025 09:43PM UTC coverage: 23.472% (+1.5%) from 21.938%
18543522652

push

github

jcubic
Base UI

64 of 98 branches covered (65.31%)

Branch coverage included in aggregate %.

228 of 853 new or added lines in 12 files covered. (26.73%)

2 existing lines in 2 files now uncovered.

439 of 2045 relevant lines covered (21.47%)

2.66 hits per line

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

80.24
/src/components/notes/LeftPanel.tsx
1
'use client';
2

3
import { useState, useEffect, useRef } from 'react';
1✔
4
import { Box, Button, Input, Stack, Text } from '@chakra-ui/react';
1✔
5
import { FiFileText, FiCircle } from 'react-icons/fi';
1✔
6
import clsx from 'clsx';
1✔
7
import type { Note } from '@prisma/client';
8
import { useNotesContext } from './NotesContext';
1✔
9

10
interface LeftPanelProps {
11
  notes: Note[];
12
  selectedNoteId: number | null;
13
  onNoteSelect: (id: number) => void;
14
  onNewNote: () => void;
15
  onDeleteNote?: (id: number) => void;
16
  onRenameNote?: (id: number, newName: string) => Promise<void>;
17
}
18

19
export default function LeftPanel({
22✔
20
  notes,
22✔
21
  selectedNoteId,
22✔
22
  onNoteSelect,
22✔
23
  onNewNote,
22✔
24
  onDeleteNote,
22✔
25
  onRenameNote
22✔
26
}: LeftPanelProps) {
22✔
27
  const [filter, setFilter] = useState('');
22✔
28
  const [focusedIndex, setFocusedIndex] = useState(-1);
22✔
29
  const [editingNoteId, setEditingNoteId] = useState<number | null>(null);
22✔
30
  const [editingName, setEditingName] = useState('');
22✔
31
  const { hasUnsavedChanges } = useNotesContext();
22✔
32
  const listRef = useRef<HTMLDivElement>(null);
22✔
33
  const editInputRef = useRef<HTMLInputElement>(null);
22✔
34

35
  const filteredNotes = notes.filter((note) =>
22✔
36
    note.name.toLowerCase().includes(filter.toLowerCase())
44✔
37
  );
22✔
38

39
  // Keyboard navigation
40
  useEffect(() => {
22✔
41
    const handleKeyDown = (e: KeyboardEvent) => {
17✔
42
      if (e.target !== listRef.current && !listRef.current?.contains(e.target as Node)) {
4!
NEW
43
        return;
×
NEW
44
      }
×
45

46
      switch (e.key) {
4✔
47
        case 'ArrowDown':
4!
NEW
48
          e.preventDefault();
×
NEW
49
          setFocusedIndex((prev) => Math.min(prev + 1, filteredNotes.length - 1));
×
NEW
50
          break;
×
51
        case 'ArrowUp':
4!
NEW
52
          e.preventDefault();
×
NEW
53
          setFocusedIndex((prev) => Math.max(prev - 1, 0));
×
NEW
54
          break;
×
55
        case 'Enter':
4✔
56
          e.preventDefault();
3✔
57
          if (focusedIndex >= 0 && focusedIndex < filteredNotes.length) {
3!
NEW
58
            onNoteSelect(filteredNotes[focusedIndex].id);
×
NEW
59
          }
×
60
          break;
3✔
61
        case 'Delete':
4!
62
        case 'Backspace':
4!
NEW
63
          e.preventDefault();
×
NEW
64
          if (focusedIndex >= 0 && focusedIndex < filteredNotes.length && onDeleteNote) {
×
NEW
65
            const noteToDelete = filteredNotes[focusedIndex];
×
NEW
66
            if (window.confirm(`Delete note "${noteToDelete.name}"?`)) {
×
NEW
67
              onDeleteNote(noteToDelete.id);
×
NEW
68
            }
×
NEW
69
          }
×
NEW
70
          break;
×
71
      }
4✔
72
    };
4✔
73

74
    window.addEventListener('keydown', handleKeyDown);
17✔
75
    return () => window.removeEventListener('keydown', handleKeyDown);
17✔
76
  }, [focusedIndex, filteredNotes, onNoteSelect, onDeleteNote]);
22✔
77

78
  // Reset focus when filter changes
79
  useEffect(() => {
22✔
80
    setFocusedIndex(-1);
6✔
81
  }, [filter]);
22✔
82

83
  // Focus edit input when editing starts
84
  useEffect(() => {
22✔
85
    if (editingNoteId && editInputRef.current) {
14✔
86
      editInputRef.current.focus();
5✔
87
      editInputRef.current.select();
5✔
88
    }
5✔
89
  }, [editingNoteId]);
22✔
90

91
  const handleDoubleClick = (note: Note) => {
22✔
92
    setEditingNoteId(note.id);
5✔
93
    setEditingName(note.name);
5✔
94
  };
5✔
95

96
  const handleRenameSubmit = async () => {
22✔
97
    if (!editingNoteId || !onRenameNote) {
3!
NEW
98
      cancelRename();
×
NEW
99
      return;
×
NEW
100
    }
×
101

102
    const trimmedName = editingName.trim();
3✔
103

104
    // Validation: empty names revert to original
105
    if (!trimmedName) {
3✔
106
      cancelRename();
1✔
107
      return;
1✔
108
    }
1✔
109

110
    // Validation: max length (matches backend limit)
111
    if (trimmedName.length > 255) {
3!
NEW
112
      setEditingName(trimmedName.substring(0, 255));
×
NEW
113
      return;
×
NEW
114
    }
✔
115

116
    // Find the original note to compare names
117
    const originalNote = notes.find((n) => n.id === editingNoteId);
2✔
118
    if (originalNote && trimmedName === originalNote.name) {
3✔
119
      // No change needed
120
      cancelRename();
1✔
121
      return;
1✔
122
    }
1✔
123

124
    try {
1✔
125
      await onRenameNote(editingNoteId, trimmedName);
1✔
126
      setEditingNoteId(null);
1✔
127
      setEditingName('');
1✔
128
    } catch (error) {
3!
NEW
129
      console.error('Failed to rename note:', error);
×
NEW
130
      cancelRename();
×
NEW
131
    }
×
132
  };
3✔
133

134
  const cancelRename = () => {
22✔
135
    setEditingNoteId(null);
3✔
136
    setEditingName('');
3✔
137
  };
3✔
138

139
  const handleEditKeyDown = (e: React.KeyboardEvent) => {
22✔
140
    if (e.key === 'Enter') {
4✔
141
      e.preventDefault();
3✔
142
      handleRenameSubmit();
3✔
143
    } else if (e.key === 'Escape') {
4✔
144
      e.preventDefault();
1✔
145
      cancelRename();
1✔
146
    }
1✔
147
  };
4✔
148

149
  return (
22✔
150
    <Box as="aside" h="100%" display="flex" flexDirection="column" p={4} bg="bg.subtle">
22✔
151
      <Stack gap={4} align="stretch">
22✔
152
        <Button colorPalette="blue" variant="solid" onClick={onNewNote}>
22✔
153
          New Note
154
        </Button>
22✔
155

156
        <Input
22✔
157
          p={3}
22✔
158
          placeholder="Filter notes..."
22✔
159
          value={filter}
22✔
160
          onChange={(e) => setFilter(e.target.value)}
22✔
161
          size="sm"
22✔
162
        />
22✔
163
      </Stack>
22✔
164

165
      <Box
22✔
166
        ref={listRef}
22✔
167
        flex={1}
22✔
168
        mt={4}
22✔
169
        overflow="auto"
22✔
170
        tabIndex={0}
22✔
171
        _focus={{ outline: 'none' }}
22✔
172
      >
173
        {filteredNotes.length === 0 ? (
22!
NEW
174
          <Text textAlign="center" color="fg.muted" fontSize="sm">
×
NEW
175
            {notes.length === 0 ? 'No notes yet' : 'No matching notes'}
×
NEW
176
          </Text>
×
177
        ) : (
178
          <Stack gap={1} align="stretch">
22✔
179
            {filteredNotes.map((note, index) => {
22✔
180
              const isSelected = selectedNoteId === note.id;
44✔
181
              const isFocused = focusedIndex === index;
44✔
182
              const hasChanges = isSelected && hasUnsavedChanges;
44!
183

184
              return (
44✔
185
                <Box
44✔
186
                  key={note.id}
44✔
187
                  p={2}
44✔
188
                  borderRadius="md"
44✔
189
                  cursor="pointer"
44✔
190
                  bg={isSelected ? 'blue.solid' : isFocused ? 'bg.muted' : 'transparent'}
44!
191
                  color={isSelected ? 'white' : 'fg'}
44!
192
                  border={isFocused ? '1px solid' : '1px solid transparent'}
44!
193
                  borderColor={isFocused ? 'border.emphasized' : 'transparent'}
44!
194
                  className={clsx('tree-node', {
44✔
195
                    'tree-node-selected': isSelected,
44✔
196
                    'tree-node-focused': isFocused,
44✔
197
                    'tree-node-leaf': true
44✔
198
                  })}
44✔
199
                  _hover={{
44✔
200
                    bg: isSelected ? 'blue.solid' : 'bg.muted'
44!
201
                  }}
44✔
202
                  onClick={() => onNoteSelect(note.id)}
44✔
203
                  data-testid={`note-item-${note.id}`}
44✔
204
                >
205
                  <Box display="flex" alignItems="center" gap={2}>
44✔
206
                    <FiFileText size={14} color="currentColor" />
44✔
207
                    {editingNoteId === note.id ? (
44✔
208
                      <Input
13✔
209
                        ref={editInputRef}
13✔
210
                        value={editingName}
13✔
211
                        onChange={(e) => setEditingName(e.target.value)}
13✔
212
                        onBlur={handleRenameSubmit}
13✔
213
                        onKeyDown={handleEditKeyDown}
13✔
214
                        fontSize="sm"
13✔
215
                        fontWeight="medium"
13✔
216
                        size="sm"
13✔
217
                        variant="flushed"
13✔
218
                        flex={1}
13✔
219
                        minH="auto"
13✔
220
                        h="auto"
13✔
221
                        p={0}
13✔
222
                        bg="transparent"
13✔
223
                        border="none"
13✔
224
                        borderBottom="1px solid"
13✔
225
                        borderColor="border.emphasized"
13✔
226
                        borderRadius={0}
13✔
227
                        _focus={{ borderColor: 'blue.500', boxShadow: 'none' }}
13✔
228
                      />
13✔
229
                    ) : (
230
                      <Text
31✔
231
                        fontSize="sm"
31✔
232
                        fontWeight="medium"
31✔
233
                        lineClamp={1}
31✔
234
                        flex={1}
31✔
235
                        onDoubleClick={() => handleDoubleClick(note)}
31✔
236
                        cursor="pointer"
31✔
237
                        userSelect="none"
31✔
238
                      >
239
                        {note.name}
31✔
240
                      </Text>
31✔
241
                    )}
242
                    {hasChanges && <FiCircle size={8} color="orange" fill="orange" />}
44!
243
                  </Box>
44✔
244
                </Box>
44✔
245
              );
246
            })}
22✔
247
          </Stack>
22✔
248
        )}
249
      </Box>
22✔
250
    </Box>
22✔
251
  );
252
}
22✔
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