Skip to content

Commit 05e5ab6

Browse files
committed
feat(editor): add debounced auto-save, save-status toast, and basic formatting actions
- refactor save flow into performSave and debounce saves with a 2s timer - track saveStatus ('saved' | 'saving' | 'unsaved') and show a transient toast in the editor UI - prevent auto-save for empty block sets; clear timers on manual save/unmount - implement simple formatting actions (bold, italic, strikethrough, code, link) applied to the current/last paragraph block fix(home): improve search filtering to: - return all notes for empty query - search title, preview, and string content inside blocks - simplify/filter logic (remove previous matchesSearch branch)
1 parent 0741848 commit 05e5ab6

File tree

2 files changed

+139
-22
lines changed

2 files changed

+139
-22
lines changed

app/editor.tsx

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,11 @@ export default function EditorScreen() {
289289
const [rawMarkdown, setRawMarkdown] = useState('');
290290
const [isEditingMarkdown, setIsEditingMarkdown] = useState(false);
291291
const [editedMarkdown, setEditedMarkdown] = useState('');
292+
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved');
292293
const isInitialLoad = useRef(true);
293294
const initialBlocksRef = useRef<EditorBlock[]>([]);
294295
const isTitleManuallySet = useRef(false);
296+
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
295297

296298
const colors = Colors[colorScheme ?? 'light'];
297299
const styles = getStyles(colorScheme ?? 'light');
@@ -420,24 +422,21 @@ export default function EditorScreen() {
420422
[markAsChanged, clearUnsavedChanges, hasUnsavedChanges]
421423
);
422424

423-
// Save note handler
424-
const handleSaveNote = useCallback(async () => {
425-
if (isSaving) return;
425+
// Auto-save with debounce
426+
const performSave = useCallback(async () => {
427+
if (isSaving || blocks.length === 0) return;
426428

427429
setIsSaving(true);
430+
setSaveStatus('saving');
428431
try {
429432
if (__DEV__) {
430433
const footnoteBlocks = blocks.filter(b => b.type === 'footnote');
431-
console.log('[handleSaveNote] Saving blocks count:', blocks.length);
434+
console.log('[performSave] Saving blocks count:', blocks.length);
432435
console.log(
433-
'[handleSaveNote] Footnote blocks:',
436+
'[performSave] Footnote blocks:',
434437
footnoteBlocks.length,
435438
footnoteBlocks.map(b => b.meta?.footnoteId)
436439
);
437-
console.log(
438-
'[handleSaveNote] Block types:',
439-
blocks.map(b => b.type)
440-
);
441440
}
442441

443442
// Generate note preview from blocks
@@ -473,14 +472,52 @@ export default function EditorScreen() {
473472
setCurrentNote(savedNote);
474473
// Update initial blocks reference after successful save
475474
initialBlocksRef.current = blocks;
476-
Alert.alert('Success', 'Note saved successfully!');
475+
clearUnsavedChanges();
476+
setSaveStatus('saved');
477+
478+
// Show saved status briefly, then hide
479+
setTimeout(() => setSaveStatus('saved'), 1000);
477480
} catch (error) {
478481
console.error('Failed to save note:', error);
479-
Alert.alert('Error', 'Failed to save note');
482+
setSaveStatus('unsaved');
480483
} finally {
481484
setIsSaving(false);
482485
}
483-
}, [blocks, currentNote, saveNote, isSaving, noteTitle]);
486+
}, [blocks, currentNote, saveNote, isSaving, noteTitle, clearUnsavedChanges, setCurrentNote]);
487+
488+
// Trigger auto-save when blocks change
489+
useEffect(() => {
490+
if (isInitialLoad.current || blocks.length === 0) {
491+
return;
492+
}
493+
494+
// Clear existing timer
495+
if (autoSaveTimerRef.current) {
496+
clearTimeout(autoSaveTimerRef.current);
497+
}
498+
499+
// Set status to unsaved immediately
500+
setSaveStatus('unsaved');
501+
502+
// Debounce save by 2 seconds
503+
autoSaveTimerRef.current = setTimeout(() => {
504+
performSave();
505+
}, 2000);
506+
507+
return () => {
508+
if (autoSaveTimerRef.current) {
509+
clearTimeout(autoSaveTimerRef.current);
510+
}
511+
};
512+
}, [blocks, performSave]);
513+
514+
// Manual save handler (for explicit save button)
515+
const handleSaveNote = useCallback(async () => {
516+
if (autoSaveTimerRef.current) {
517+
clearTimeout(autoSaveTimerRef.current);
518+
}
519+
await performSave();
520+
}, [performSave]);
484521

485522
// Handle undo
486523
const handleUndo = useCallback(() => {
@@ -498,9 +535,49 @@ export default function EditorScreen() {
498535

499536
// Handle formatting actions
500537
const handleFormattingAction = useCallback((actionId: string) => {
501-
console.log('Formatting action:', actionId);
502-
// TODO: Implement formatting actions
503-
}, []);
538+
if (!editorRef.current) return;
539+
540+
// Get current blocks
541+
const currentBlocks = editorRef.current.getBlocks();
542+
if (currentBlocks.length === 0) return;
543+
544+
// For now, apply formatting to the last block (where cursor likely is)
545+
const lastBlock = currentBlocks[currentBlocks.length - 1];
546+
if (!lastBlock || lastBlock.type !== 'paragraph') return;
547+
548+
const content = lastBlock.content;
549+
let newContent = content;
550+
551+
switch (actionId) {
552+
case 'bold':
553+
newContent = content.includes('**') ? content.replace(/\*\*/g, '') : `**${content}**`;
554+
break;
555+
case 'italic':
556+
newContent = content.includes('*') && !content.includes('**') ? content.replace(/\*/g, '') : `*${content}*`;
557+
break;
558+
case 'strikethrough':
559+
newContent = content.includes('~~') ? content.replace(/~~/g, '') : `~~${content}~~`;
560+
break;
561+
case 'code':
562+
newContent = content.includes('`') ? content.replace(/`/g, '') : `\`${content}\``;
563+
break;
564+
case 'link':
565+
if (!content.includes('[')) {
566+
newContent = `[${content}](url)`;
567+
}
568+
break;
569+
default:
570+
return;
571+
}
572+
573+
// Update blocks array
574+
const updatedBlocks = [...currentBlocks];
575+
updatedBlocks[updatedBlocks.length - 1] = { ...lastBlock, content: newContent };
576+
577+
// Apply via setBlocks
578+
setBlocks(updatedBlocks);
579+
markAsChanged();
580+
}, [markAsChanged]);
504581

505582
// Handle rename
506583
const handleRename = useCallback(() => {
@@ -697,6 +774,16 @@ export default function EditorScreen() {
697774
barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
698775
backgroundColor={colors.background}
699776
/>
777+
778+
{/* Save Status Toast */}
779+
{saveStatus !== 'saved' && (
780+
<View style={[styles.saveToast, { backgroundColor: colorScheme === 'dark' ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.95)' }]}>
781+
<Text style={[styles.saveToastText, { color: colors.text }]}>
782+
{saveStatus === 'saving' ? '💾 Saving...' : saveStatus === 'unsaved' ? '✏️ Unsaved changes' : ''}
783+
</Text>
784+
</View>
785+
)}
786+
700787
{/* Compact Header */}
701788
<View style={styles.header}>
702789
<View style={styles.headerRow}>
@@ -997,6 +1084,24 @@ const getStyles = (colorScheme: 'light' | 'dark') => {
9971084
saveButton: {
9981085
backgroundColor: colors.tint,
9991086
},
1087+
saveToast: {
1088+
position: 'absolute',
1089+
top: 60,
1090+
alignSelf: 'center',
1091+
paddingHorizontal: 16,
1092+
paddingVertical: 8,
1093+
borderRadius: 20,
1094+
zIndex: 1000,
1095+
shadowColor: '#000',
1096+
shadowOffset: { width: 0, height: 2 },
1097+
shadowOpacity: 0.1,
1098+
shadowRadius: 4,
1099+
elevation: 4,
1100+
},
1101+
saveToastText: {
1102+
fontSize: 13,
1103+
fontFamily: 'AlbertSans_500Medium',
1104+
},
10001105
loadingContainer: {
10011106
justifyContent: 'center',
10021107
alignItems: 'center',

app/index.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,28 @@ export default function HomeScreen() {
112112
};
113113

114114
const filteredNotes = notes.filter(note => {
115-
const matchesSearch =
116-
note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
117-
note.preview.toLowerCase().includes(searchQuery.toLowerCase());
115+
if (!searchQuery) return true;
118116

119-
if (activeFilter === 'Recent') {
120-
const isRecent = Date.now() - note.lastModified.getTime() < 7 * 24 * 60 * 60 * 1000; // 7 days
121-
return matchesSearch && isRecent;
117+
const query = searchQuery.toLowerCase();
118+
119+
// Search in title
120+
if (note.title.toLowerCase().includes(query)) return true;
121+
122+
// Search in preview
123+
if (note.preview.toLowerCase().includes(query)) return true;
124+
125+
// Search in block content
126+
if (note.content && Array.isArray(note.content)) {
127+
const hasMatch = note.content.some((block: any) => {
128+
if (typeof block.content === 'string') {
129+
return block.content.toLowerCase().includes(query);
130+
}
131+
return false;
132+
});
133+
if (hasMatch) return true;
122134
}
123135

124-
return matchesSearch;
136+
return false;
125137
});
126138

127139
const renderNoteItem = ({ item }: { item: (typeof notes)[0] }) => (

0 commit comments

Comments
 (0)