@mytec: iter1.1.1 ready for testing

This commit is contained in:
2026-01-30 23:15:51 +02:00
parent 0fb19476cd
commit b7d008fe26
8 changed files with 422 additions and 11 deletions

View File

@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react';
import { useProjectsStore } from '@/store/projects.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import { useDirtyStore } from '@/hooks/useUnsavedChanges.ts';
import Button from '@/components/ui/Button.tsx';
import ConfirmDialog from '@/components/ui/ConfirmDialog.tsx';
import { logger } from '@/utils/logger.ts';
export default function ProjectPanel() {
@@ -12,11 +14,19 @@ export default function ProjectPanel() {
const loadProject = useProjectsStore((s) => s.loadProject);
const deleteProject = useProjectsStore((s) => s.deleteProject);
const addToast = useToastStore((s) => s.addToast);
const isDirty = useDirtyStore((s) => s.isDirty);
const [projectName, setProjectName] = useState('');
const [showSaveForm, setShowSaveForm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// Confirm dialog state
const [confirmAction, setConfirmAction] = useState<{
type: 'load' | 'delete';
id: string;
name: string;
} | null>(null);
useEffect(() => {
loadProjects();
}, [loadProjects]);
@@ -30,6 +40,7 @@ export default function ProjectPanel() {
try {
await saveProject(name);
useDirtyStore.getState().markClean();
addToast(`Project "${name}" saved`, 'success');
setProjectName('');
setShowSaveForm(false);
@@ -39,11 +50,20 @@ export default function ProjectPanel() {
}
};
const handleLoad = async (id: string, name: string) => {
const handleLoadRequest = (id: string, name: string) => {
if (isDirty) {
setConfirmAction({ type: 'load', id, name });
} else {
executeLoad(id, name);
}
};
const executeLoad = async (id: string, name: string) => {
setIsLoading(true);
try {
const project = await loadProject(id);
if (project) {
useDirtyStore.getState().markClean();
addToast(`Loaded project "${name}" (${project.sites.length} sites)`, 'success');
} else {
addToast('Project not found', 'error');
@@ -56,7 +76,11 @@ export default function ProjectPanel() {
}
};
const handleDelete = async (id: string, name: string) => {
const handleDeleteRequest = (id: string, name: string) => {
setConfirmAction({ type: 'delete', id, name });
};
const executeDelete = async (id: string, name: string) => {
try {
await deleteProject(id);
addToast(`Project "${name}" deleted`, 'info');
@@ -66,6 +90,18 @@ export default function ProjectPanel() {
}
};
const handleConfirm = async () => {
if (!confirmAction) return;
const { type, id, name } = confirmAction;
setConfirmAction(null);
if (type === 'load') {
await executeLoad(id, name);
} else {
await executeDelete(id, name);
}
};
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleDateString(undefined, {
month: 'short',
@@ -142,7 +178,7 @@ export default function ProjectPanel() {
</div>
<div className="flex gap-1 flex-shrink-0">
<button
onClick={() => handleLoad(project.id, project.name)}
onClick={() => handleLoadRequest(project.id, project.name)}
disabled={isLoading}
className="px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded
min-w-[40px] min-h-[32px] flex items-center justify-center disabled:opacity-50"
@@ -150,7 +186,7 @@ export default function ProjectPanel() {
Load
</button>
<button
onClick={() => handleDelete(project.id, project.name)}
onClick={() => handleDeleteRequest(project.id, project.name)}
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded
min-w-[32px] min-h-[32px] flex items-center justify-center"
>
@@ -161,6 +197,27 @@ export default function ProjectPanel() {
))}
</div>
)}
{/* Confirmation dialog */}
{confirmAction && (
<ConfirmDialog
title={
confirmAction.type === 'delete'
? 'Delete Project?'
: 'Unsaved Changes'
}
message={
confirmAction.type === 'delete'
? `Delete project "${confirmAction.name}"? This cannot be undone.`
: `You have unsaved changes. Load project "${confirmAction.name}" anyway?`
}
confirmLabel={confirmAction.type === 'delete' ? 'Delete' : 'Load'}
cancelLabel="Cancel"
variant={confirmAction.type === 'delete' ? 'danger' : 'warning'}
onConfirm={handleConfirm}
onCancel={() => setConfirmAction(null)}
/>
)}
</div>
);
}

View File

@@ -7,9 +7,17 @@ interface ConfirmDialogProps {
cancelLabel?: string;
onConfirm: () => void;
onCancel: () => void;
/** @deprecated Use variant instead */
danger?: boolean;
variant?: 'danger' | 'warning' | 'info';
}
const VARIANT_STYLES = {
danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
warning: 'bg-amber-500 hover:bg-amber-600 focus:ring-amber-400',
info: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500',
} as const;
export default function ConfirmDialog({
title,
message,
@@ -18,11 +26,15 @@ export default function ConfirmDialog({
onConfirm,
onCancel,
danger = false,
variant,
}: ConfirmDialogProps) {
const confirmRef = useRef<HTMLButtonElement>(null);
// Resolve variant: explicit variant prop takes priority, then legacy danger boolean
const resolvedVariant = variant ?? (danger ? 'danger' : 'info');
useEffect(() => {
// Auto-focus the cancel button (safer default)
// Auto-focus the confirm button
confirmRef.current?.focus();
const handleKey = (e: KeyboardEvent) => {
@@ -67,10 +79,7 @@ export default function ConfirmDialog({
ref={confirmRef}
onClick={onConfirm}
className={`px-4 py-2 text-sm rounded-md text-white transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2
${danger
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'
}`}
${VARIANT_STYLES[resolvedVariant]}`}
>
{confirmLabel}
</button>