Skip to content

Commit d21c1c6

Browse files
committed
fix debouncing on notes
1 parent 5d3a629 commit d21c1c6

File tree

1 file changed

+63
-6
lines changed

1 file changed

+63
-6
lines changed

components/MobileDashboard.tsx

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useCallback, useEffect, useState } from "react";
3+
import { useCallback, useEffect, useRef, useState } from "react";
44
import { X, Github, FileText, Shield, Coffee, Smartphone, Sparkles, LogOut, ExternalLink, Upload, Download, BarChart3 } from "lucide-react";
55
import { signOut } from "next-auth/react";
66
import { Capacitor } from "@capacitor/core";
@@ -96,6 +96,9 @@ export default function MobileDashboard() {
9696
const [wscaSyncing, setWscaSyncing] = useState(false);
9797
const [wscaSyncStatus, setWscaSyncStatus] = useState<string | null>(null);
9898
const importInputId = "moltly-import-input";
99+
const noteSaveTimers = useRef<Record<string, number>>({});
100+
const pendingNoteUpdates = useRef<Record<string, ResearchNote[]>>({});
101+
const noteSaveLatestRequest = useRef<Record<string, number>>({});
99102

100103
// Cross-platform export helper: uses Capacitor on native platforms, falls back to web download
101104
const exportJsonText = useCallback(async (jsonText: string) => {
@@ -189,6 +192,12 @@ export default function MobileDashboard() {
189192
return () => window.removeEventListener("scroll", onScroll);
190193
}, []);
191194

195+
useEffect(() => {
196+
return () => {
197+
Object.values(noteSaveTimers.current).forEach((timer) => window.clearTimeout(timer));
198+
};
199+
}, []);
200+
192201
const openNewEntry = () => {
193202
setEditingId(null);
194203
setFormState(defaultForm());
@@ -638,7 +647,17 @@ export default function MobileDashboard() {
638647
setStacks((prev) => prev.filter((s) => s.id !== id));
639648
};
640649

650+
const cancelPendingNoteSave = (stackId: string) => {
651+
if (noteSaveTimers.current[stackId]) {
652+
window.clearTimeout(noteSaveTimers.current[stackId]);
653+
delete noteSaveTimers.current[stackId];
654+
}
655+
delete pendingNoteUpdates.current[stackId];
656+
noteSaveLatestRequest.current[stackId] = Date.now();
657+
};
658+
641659
const onCreateNote = async (stackId: string, note: Partial<ResearchNote>) => {
660+
cancelPendingNoteSave(stackId);
642661
const base: ResearchNote = {
643662
id: crypto.randomUUID(),
644663
title: (note.title ?? "New Note").trim() || "New Note",
@@ -654,21 +673,59 @@ export default function MobileDashboard() {
654673
await onUpdateStack(stackId, { notes: updated.notes, name: stack.name });
655674
};
656675

657-
const onUpdateNote = async (stackId: string, noteId: string, updates: Partial<ResearchNote>) => {
658-
const stack = stacks.find((s) => s.id === stackId);
659-
if (!stack) return;
660-
const notes = stack.notes.map((n) => (n.id === noteId ? { ...n, ...updates, updatedAt: new Date().toISOString() } : n));
661-
await onUpdateStack(stackId, { notes, name: stack.name });
676+
const onUpdateNote = (stackId: string, noteId: string, updates: Partial<ResearchNote>) => {
677+
if (!mode) return;
678+
679+
const now = new Date().toISOString();
680+
let nextNotes: ResearchNote[] | null = null;
681+
682+
setStacks((prev) =>
683+
prev.map((s) => {
684+
if (s.id !== stackId) return s;
685+
nextNotes = s.notes.map((n) => (n.id === noteId ? { ...n, ...updates, updatedAt: now } : n));
686+
return { ...s, notes: nextNotes, updatedAt: now };
687+
})
688+
);
689+
690+
if (!nextNotes) return;
691+
pendingNoteUpdates.current[stackId] = nextNotes;
692+
693+
if (!isSync) return;
694+
695+
if (noteSaveTimers.current[stackId]) {
696+
window.clearTimeout(noteSaveTimers.current[stackId]);
697+
}
698+
699+
noteSaveTimers.current[stackId] = window.setTimeout(async () => {
700+
const requestId = Date.now();
701+
noteSaveLatestRequest.current[stackId] = requestId;
702+
const payload = pendingNoteUpdates.current[stackId] ?? nextNotes;
703+
try {
704+
const res = await fetch(`/api/research/${stackId}`, {
705+
method: "PATCH",
706+
headers: { "Content-Type": "application/json" },
707+
body: JSON.stringify({ notes: payload }),
708+
});
709+
if (!res.ok) return;
710+
const saved = (await res.json()) as ResearchStack;
711+
if (noteSaveLatestRequest.current[stackId] !== requestId) return;
712+
setStacks((prev) => prev.map((s) => (s.id === stackId ? saved : s)));
713+
} catch (err) {
714+
console.error(err);
715+
}
716+
}, 400);
662717
};
663718

664719
const onDeleteNote = async (stackId: string, noteId: string) => {
720+
cancelPendingNoteSave(stackId);
665721
const stack = stacks.find((s) => s.id === stackId);
666722
if (!stack) return;
667723
const notes = stack.notes.filter((n) => n.id !== noteId);
668724
await onUpdateStack(stackId, { notes, name: stack.name });
669725
};
670726

671727
const onDuplicateNote = async (stackId: string, noteId: string) => {
728+
cancelPendingNoteSave(stackId);
672729
const stack = stacks.find((s) => s.id === stackId);
673730
if (!stack) return;
674731
const src = stack.notes.find((n) => n.id === noteId);

0 commit comments

Comments
 (0)