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

jcubic / 10xDevs / 18544834343

15 Oct 2025 10:52PM UTC coverage: 20.127% (+0.05%) from 20.079%
18544834343

push

github

jcubic
fix linting

67 of 102 branches covered (65.69%)

Branch coverage included in aggregate %.

0 of 10 new or added lines in 2 files covered. (0.0%)

26 existing lines in 3 files now uncovered.

440 of 2417 relevant lines covered (18.2%)

2.3 hits per line

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

80.32
/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({
23✔
20
  notes,
23✔
21
  selectedNoteId,
23✔
22
  onNoteSelect,
23✔
23
  onNewNote,
23✔
24
  onDeleteNote,
23✔
25
  onRenameNote
23✔
26
}: LeftPanelProps) {
23✔
27
  const [filter, setFilter] = useState('');
23✔
28
  const [focusedIndex, setFocusedIndex] = useState(-1);
23✔
29
  const [editingNoteId, setEditingNoteId] = useState<number | null>(null);
23✔
30
  const [editingName, setEditingName] = useState('');
23✔
31

32
  const { hasUnsavedChanges } = useNotesContext();
23✔
33
  const listRef = useRef<HTMLDivElement>(null);
23✔
34
  const editInputRef = useRef<HTMLInputElement>(null);
23✔
35

36
  const filteredNotes = notes
23✔
37
    .filter((note) => note.name.toLowerCase().includes(filter.toLowerCase()))
23✔
38
    .sort((a, b) => a.id - b.id);
23✔
39

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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