From f5c0a0b2f7459b3701b581c90d67178b0560c8b0 Mon Sep 17 00:00:00 2001 From: "simon.franken" Date: Wed, 18 Feb 2026 15:26:36 +0100 Subject: [PATCH] improvements --- frontend/src/components/ConfirmModal.tsx | 35 +++++ frontend/src/components/Modal.tsx | 15 +- .../src/components/TimeEntryFormModal.tsx | 122 ++++++++++++++++ frontend/src/components/TimerWidget.tsx | 101 ++++++------- frontend/src/pages/ClientsPage.tsx | 35 ++++- frontend/src/pages/DashboardPage.tsx | 68 ++++++++- frontend/src/pages/ProjectsPage.tsx | 20 ++- frontend/src/pages/TimeEntriesPage.tsx | 137 ++++-------------- 8 files changed, 354 insertions(+), 179 deletions(-) create mode 100644 frontend/src/components/ConfirmModal.tsx create mode 100644 frontend/src/components/TimeEntryFormModal.tsx diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..1fb9b6f --- /dev/null +++ b/frontend/src/components/ConfirmModal.tsx @@ -0,0 +1,35 @@ +import { Modal } from '@/components/Modal'; + +interface ConfirmModalProps { + title: string; + message: string; + confirmLabel?: string; + onConfirm: () => void; + onClose: () => void; +} + +export function ConfirmModal({ + title, + message, + confirmLabel = 'Delete', + onConfirm, + onClose, +}: ConfirmModalProps) { + return ( + +

{message}

+
+ + +
+
+ ); +} diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index d45b8ae..1ccd602 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { X } from 'lucide-react'; interface ModalProps { @@ -10,6 +11,7 @@ interface ModalProps { /** * Generic modal overlay with a header and close button. + * Closes on Escape key or backdrop click. * Render form content (or any JSX) as children. * * @example @@ -18,8 +20,19 @@ interface ModalProps { * */ export function Modal({ title, onClose, children, maxWidth = 'max-w-md' }: ModalProps) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + return ( -
+
{ if (e.target === e.currentTarget) onClose(); }} + >

{title}

diff --git a/frontend/src/components/TimeEntryFormModal.tsx b/frontend/src/components/TimeEntryFormModal.tsx new file mode 100644 index 0000000..ad949ee --- /dev/null +++ b/frontend/src/components/TimeEntryFormModal.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { Modal } from '@/components/Modal'; +import { useProjects } from '@/hooks/useProjects'; +import { getLocalISOString, toISOTimezone } from '@/utils/dateUtils'; +import type { TimeEntry, CreateTimeEntryInput, UpdateTimeEntryInput } from '@/types'; +import type { UseMutationResult } from '@tanstack/react-query'; + +interface TimeEntryFormModalProps { + entry: TimeEntry | null; + onClose: () => void; + createTimeEntry: UseMutationResult; + updateTimeEntry: UseMutationResult; +} + +export function TimeEntryFormModal({ entry, onClose, createTimeEntry, updateTimeEntry }: TimeEntryFormModalProps) { + const { projects } = useProjects(); + + const [formData, setFormData] = useState(() => { + if (entry) { + return { + startTime: getLocalISOString(new Date(entry.startTime)), + endTime: getLocalISOString(new Date(entry.endTime)), + description: entry.description || '', + projectId: entry.projectId, + }; + } + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); + return { + startTime: getLocalISOString(oneHourAgo), + endTime: getLocalISOString(now), + description: '', + projectId: projects?.[0]?.id || '', + }; + }); + + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!formData.projectId) { + setError('Please select a project'); + return; + } + + const input = { + ...formData, + startTime: toISOTimezone(formData.startTime), + endTime: toISOTimezone(formData.endTime), + }; + + try { + if (entry) { + await updateTimeEntry.mutateAsync({ id: entry.id, input: input as UpdateTimeEntryInput }); + } else { + await createTimeEntry.mutateAsync(input); + } + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save'); + } + }; + + return ( + + {error &&
{error}
} +
+
+ + +
+
+
+ + setFormData({ ...formData, startTime: e.target.value })} + className="input" + /> +
+
+ + setFormData({ ...formData, endTime: e.target.value })} + className="input" + /> +
+
+
+ +