Add cancel (discard) timer feature

Allows users to discard a running timer without creating a time entry.
A trash icon in the timer widget reveals a confirmation step ('Discard / Keep')
to prevent accidental data loss. Backend exposes a new DELETE /api/timer
endpoint that simply deletes the ongoingTimer row.
This commit is contained in:
simon.franken
2026-02-23 10:41:50 +01:00
parent 7358fa6256
commit 06596dcee9
5 changed files with 89 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useRef } from "react";
import { Play, Square, ChevronDown, Pencil, Check, X } from "lucide-react";
import { Play, Square, ChevronDown, Pencil, Check, X, Trash2 } from "lucide-react";
import { useTimer } from "@/contexts/TimerContext";
import { useProjects } from "@/hooks/useProjects";
import { ProjectColorDot } from "@/components/ProjectColorDot";
@@ -49,12 +49,14 @@ export function TimerWidget() {
elapsedSeconds,
startTimer,
stopTimer,
cancelTimer,
updateTimerProject,
updateTimerStartTime,
} = useTimer();
const { projects } = useProjects();
const [showProjectSelect, setShowProjectSelect] = useState(false);
const [error, setError] = useState<string | null>(null);
const [confirmCancel, setConfirmCancel] = useState(false);
// Start time editing state
const [editingStartTime, setEditingStartTime] = useState(false);
@@ -72,6 +74,7 @@ export function TimerWidget() {
const handleStop = async () => {
setError(null);
setConfirmCancel(false);
try {
await stopTimer();
} catch (err) {
@@ -79,6 +82,16 @@ export function TimerWidget() {
}
};
const handleCancelTimer = async () => {
setError(null);
try {
await cancelTimer();
setConfirmCancel(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to cancel timer");
}
};
const handleProjectChange = async (projectId: string) => {
setError(null);
try {
@@ -195,14 +208,44 @@ export function TimerWidget() {
)}
</div>
{/* Stop Button */}
<button
onClick={handleStop}
className="flex items-center space-x-2 px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors shrink-0 sm:order-last"
>
<Square className="h-5 w-5 fill-current" />
<span>Stop</span>
</button>
{/* Stop + Cancel Buttons */}
<div className="flex items-center space-x-2 shrink-0 sm:order-last">
{confirmCancel ? (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">Discard timer?</span>
<button
onClick={() => void handleCancelTimer()}
className="flex items-center space-x-1 px-3 py-2 bg-gray-700 text-white rounded-lg font-medium hover:bg-gray-800 transition-colors text-sm"
>
<Trash2 className="h-4 w-4" />
<span>Discard</span>
</button>
<button
onClick={() => setConfirmCancel(false)}
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
>
Keep
</button>
</div>
) : (
<>
<button
onClick={() => setConfirmCancel(true)}
title="Discard timer"
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<Trash2 className="h-5 w-5" />
</button>
<button
onClick={handleStop}
className="flex items-center space-x-2 px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
>
<Square className="h-5 w-5 fill-current" />
<span>Stop</span>
</button>
</>
)}
</div>
</div>
{/* Project Selector — full width on mobile, auto on desktop */}