refactoring

This commit is contained in:
simon.franken
2026-02-18 10:26:15 +01:00
parent 27ec450d3b
commit 6a6a3ba00b
18 changed files with 386 additions and 371 deletions

View File

@@ -0,0 +1,39 @@
import { X } from 'lucide-react';
interface ModalProps {
title: string;
onClose: () => void;
children: React.ReactNode;
/** Optional override for the max-width of the modal panel (default: max-w-md) */
maxWidth?: string;
}
/**
* Generic modal overlay with a header and close button.
* Render form content (or any JSX) as children.
*
* @example
* <Modal title="Add Client" onClose={handleClose}>
* <form onSubmit={handleSubmit}>...</form>
* </Modal>
*/
export function Modal({ title, onClose, children, maxWidth = 'max-w-md' }: ModalProps) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className={`bg-white rounded-lg shadow-xl ${maxWidth} w-full`}>
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
<button
type="button"
onClick={onClose}
className="p-1 text-gray-400 hover:text-gray-600 rounded"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="p-6">{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
interface ProjectColorDotProps {
color: string | null | undefined;
/** Tailwind size classes (default: w-3 h-3) */
size?: string;
}
/** A small filled circle used to represent a project's colour. */
export function ProjectColorDot({ color, size = 'w-3 h-3' }: ProjectColorDotProps) {
return (
<div
className={`${size} rounded-full flex-shrink-0`}
style={{ backgroundColor: color || '#6b7280' }}
/>
);
}

View File

@@ -1,5 +1,6 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Spinner } from '@/components/Spinner';
interface ProtectedRouteProps {
children: React.ReactNode;
@@ -11,7 +12,7 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
<Spinner />
</div>
);
}

View File

@@ -0,0 +1,12 @@
interface SpinnerProps {
/** Height/width class (default: h-12 w-12) */
size?: string;
}
export function Spinner({ size = 'h-12 w-12' }: SpinnerProps) {
return (
<div className="flex items-center justify-center h-64">
<div className={`animate-spin rounded-full ${size} border-b-2 border-primary-600`} />
</div>
);
}

View File

@@ -0,0 +1,29 @@
interface StatCardProps {
icon: React.ElementType;
label: string;
value: string;
color: 'blue' | 'green' | 'purple' | 'orange';
}
const colorClasses: Record<StatCardProps['color'], string> = {
blue: 'bg-blue-50 text-blue-600',
green: 'bg-green-50 text-green-600',
purple: 'bg-purple-50 text-purple-600',
orange: 'bg-orange-50 text-orange-600',
};
export function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
return (
<div className="card p-4">
<div className="flex items-center">
<div className={`p-3 rounded-lg ${colorClasses[color]}`}>
<Icon className="h-6 w-6" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">{label}</p>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
</div>
</div>
);
}

View File

@@ -3,6 +3,7 @@ import { Play, Square, ChevronDown } from 'lucide-react';
import { useTimer } from '@/contexts/TimerContext';
import { useProjects } from '@/hooks/useProjects';
import { formatDuration } from '@/utils/dateUtils';
import { ProjectColorDot } from '@/components/ProjectColorDot';
export function TimerWidget() {
const { ongoingTimer, isLoading, elapsedSeconds, startTimer, stopTimer, updateTimerProject } = useTimer();
@@ -80,10 +81,7 @@ export function TimerWidget() {
>
{ongoingTimer.project ? (
<>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: ongoingTimer.project.color || '#6b7280' }}
/>
<ProjectColorDot color={ongoingTimer.project.color} />
<span className="text-sm font-medium text-gray-700">
{ongoingTimer.project.name}
</span>
@@ -110,10 +108,7 @@ export function TimerWidget() {
onClick={() => handleProjectChange(project.id)}
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center space-x-2"
>
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: project.color || '#6b7280' }}
/>
<ProjectColorDot color={project.color} />
<div className="min-w-0">
<div className="font-medium text-gray-900 truncate">{project.name}</div>
<div className="text-xs text-gray-500 truncate">{project.client.name}</div>