@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

@@ -9,6 +9,8 @@ interface ShortcutHandlers {
onCloseForm: () => void;
onShowShortcuts?: () => void;
onDeleteRequest?: (siteId: string, siteName: string) => void;
onUndo?: () => void;
onRedo?: () => void;
}
function isInputActive(): boolean {
@@ -23,6 +25,8 @@ export function useKeyboardShortcuts({
onCloseForm,
onShowShortcuts,
onDeleteRequest,
onUndo,
onRedo,
}: ShortcutHandlers) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -36,6 +40,20 @@ export function useKeyboardShortcuts({
return;
}
// Undo: Ctrl+Z (or Cmd+Z on Mac)
if (modKey && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
onUndo?.();
return;
}
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
if (modKey && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
e.preventDefault();
onRedo?.();
return;
}
// Escape always works
if (e.key === 'Escape') {
useSitesStore.getState().selectSite(null);
@@ -118,5 +136,5 @@ export function useKeyboardShortcuts({
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest]);
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest, onUndo, onRedo]);
}

View File

@@ -0,0 +1,76 @@
import { useEffect, useRef, useCallback } from 'react';
import { useSitesStore } from '@/store/sites.ts';
import { useCoverageStore } from '@/store/coverage.ts';
import { create } from 'zustand';
interface DirtyState {
isDirty: boolean;
lastSavedSnapshot: string | null;
markDirty: () => void;
markClean: () => void;
setSnapshot: (snapshot: string) => void;
}
export const useDirtyStore = create<DirtyState>((set) => ({
isDirty: false,
lastSavedSnapshot: null,
markDirty: () => set({ isDirty: true }),
markClean: () => set({ isDirty: false }),
setSnapshot: (snapshot: string) => set({ lastSavedSnapshot: snapshot, isDirty: false }),
}));
function getCurrentSnapshot(): string {
const sites = useSitesStore.getState().sites;
const settings = useCoverageStore.getState().settings;
return JSON.stringify({ sites: sites.map((s) => s.id), settings });
}
/**
* Hook that tracks unsaved changes and warns on page unload.
* Call markClean() after a successful save.
*/
export function useUnsavedChanges() {
const isDirty = useDirtyStore((s) => s.isDirty);
const prevSnapshot = useRef<string | null>(null);
// Take initial snapshot
useEffect(() => {
prevSnapshot.current = getCurrentSnapshot();
}, []);
// Watch sites array for changes and mark dirty
useEffect(() => {
const unsub = useSitesStore.subscribe((state) => {
const snap = JSON.stringify(state.sites.map((s) => s.id));
if (prevSnapshot.current !== null && snap !== prevSnapshot.current) {
useDirtyStore.getState().markDirty();
}
prevSnapshot.current = snap;
});
return unsub;
}, []);
// beforeunload warning
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (useDirtyStore.getState().isDirty) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, []);
const markClean = useCallback(() => {
prevSnapshot.current = getCurrentSnapshot();
useDirtyStore.getState().markClean();
}, []);
const markDirty = useCallback(() => {
useDirtyStore.getState().markDirty();
}, []);
return { isDirty, markClean, markDirty };
}