Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/app/api/habits/[id]/log/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@ export async function POST(
}
const date = startOfDay(new Date(parsed.data.date));

// Toggle: if log exists, remove it; otherwise create it
const existing = await HabitLog.findOne({
habitId: id,
date,
});
// Atomic toggle: findOneAndDelete returns the doc if it existed, null otherwise.
// This eliminates the race condition where two concurrent requests both see no
// existing log and both create one.
const deleted = await HabitLog.findOneAndDelete({ habitId: id, date });

if (existing) {
await HabitLog.findByIdAndDelete(existing._id);
if (deleted) {
return NextResponse.json({ toggled: false });
}

Expand Down
52 changes: 52 additions & 0 deletions src/hooks/use-undo-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { toast } from "sonner";

const UNDO_DELAY_MS = 4000;

/**
* Returns a `deleteWithUndo` function that:
* 1. Removes the item from UI immediately (optimistic)
* 2. Shows a "Deleted — Undo" toast for 4 seconds
* 3. If the user clicks Undo → restores the item, no API call made
* 4. If the timer expires → calls onConfirmDelete() to hit the server
*/
export function useUndoDelete() {
function deleteWithUndo({
label = "Entry deleted",
onRemoveFromUI,
onRestoreToUI,
onConfirmDelete,
}: {
label?: string;
onRemoveFromUI: () => void;
onRestoreToUI: () => void;
onConfirmDelete: () => Promise<void>;
}) {
onRemoveFromUI();

let undone = false;

const timerId = setTimeout(async () => {
if (undone) return;
try {
await onConfirmDelete();
} catch {
toast.error("Failed to delete — please try again");
onRestoreToUI();
}
}, UNDO_DELAY_MS);

toast(label, {
duration: UNDO_DELAY_MS,
action: {
label: "Undo",
onClick: () => {
undone = true;
clearTimeout(timerId);
onRestoreToUI();
},
},
});
}

return { deleteWithUndo };
}
Loading