refactoring
This commit is contained in:
39
frontend/src/components/Modal.tsx
Normal file
39
frontend/src/components/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
frontend/src/components/ProjectColorDot.tsx
Normal file
15
frontend/src/components/ProjectColorDot.tsx
Normal 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' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
12
frontend/src/components/Spinner.tsx
Normal file
12
frontend/src/components/Spinner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
frontend/src/components/StatCard.tsx
Normal file
29
frontend/src/components/StatCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user