+
{batch.isRunning && (
diff --git a/app/hooks/useBatchMode.ts b/app/hooks/useBatchMode.ts
index df9c3ef..9a43794 100644
--- a/app/hooks/useBatchMode.ts
+++ b/app/hooks/useBatchMode.ts
@@ -126,6 +126,21 @@ export const useBatchMode = (
return !isNaN(frequency) && frequency > 0 && !isNaN(repeats) && repeats > 0;
};
+ const getConfig = () => ({
+ enabled: state.enabled,
+ frequency: state.frequency,
+ repeatCount: state.repeatCount,
+ });
+
+ const loadConfig = (config: { enabled: boolean; frequency: string; repeatCount: string }) => {
+ setState((s) => ({
+ ...s,
+ enabled: config.enabled,
+ frequency: config.frequency,
+ repeatCount: config.repeatCount,
+ }));
+ };
+
return {
...state,
setEnabled,
@@ -135,5 +150,7 @@ export const useBatchMode = (
execute,
stop,
validate,
+ getConfig,
+ loadConfig,
};
};
diff --git a/app/hooks/useConfigStorage.ts b/app/hooks/useConfigStorage.ts
new file mode 100644
index 0000000..0897e83
--- /dev/null
+++ b/app/hooks/useConfigStorage.ts
@@ -0,0 +1,181 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+
+export interface ConfigData {
+ dsn: string;
+ message: string;
+ priority: 'HIGH' | 'MEDIUM' | 'LOW';
+ tags: Array<{ key: string; value: string }>;
+ errorCount: string;
+ errorsToGenerate: string;
+ fingerprintID: string;
+ batch: {
+ enabled: boolean;
+ frequency: string;
+ repeatCount: string;
+ };
+}
+
+interface SavedConfigs {
+ [name: string]: ConfigData;
+}
+
+const STORAGE_KEY_CURRENT = 'error-generator:current';
+const STORAGE_KEY_SAVED = 'error-generator:saved';
+const STORAGE_KEY_ACTIVE = 'error-generator:active';
+
+const safeGetItem = (key: string): string | null => {
+ try {
+ return localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+};
+
+const safeSetItem = (key: string, value: string): void => {
+ try {
+ localStorage.setItem(key, value);
+ } catch {
+ // localStorage full or unavailable
+ }
+};
+
+const safeRemoveItem = (key: string): void => {
+ try {
+ localStorage.removeItem(key);
+ } catch {
+ // localStorage unavailable
+ }
+};
+
+const defaultConfig: ConfigData = {
+ dsn: '',
+ message: '',
+ priority: 'HIGH',
+ tags: [],
+ errorCount: '1',
+ errorsToGenerate: '1',
+ fingerprintID: '',
+ batch: {
+ enabled: false,
+ frequency: '30',
+ repeatCount: '5',
+ },
+};
+
+const isValidConfig = (data: unknown): data is ConfigData => {
+ if (!data || typeof data !== 'object') return false;
+ const d = data as Record;
+ return (
+ typeof d.dsn === 'string' &&
+ typeof d.message === 'string' &&
+ ['HIGH', 'MEDIUM', 'LOW'].includes(d.priority as string) &&
+ Array.isArray(d.tags) &&
+ typeof d.errorCount === 'string' &&
+ typeof d.errorsToGenerate === 'string' &&
+ typeof d.fingerprintID === 'string' &&
+ d.batch !== null &&
+ typeof d.batch === 'object'
+ );
+};
+
+export const useConfigStorage = () => {
+ const [mounted, setMounted] = useState(false);
+ const [currentConfig, setCurrentConfig] = useState(defaultConfig);
+ const [savedConfigs, setSavedConfigs] = useState({});
+ const [activeConfigName, setActiveConfigName] = useState(null);
+ const saveTimeoutRef = useRef(null);
+
+ useEffect(() => {
+ setMounted(true);
+ const storedCurrent = safeGetItem(STORAGE_KEY_CURRENT);
+ if (storedCurrent) {
+ try {
+ const parsed = JSON.parse(storedCurrent);
+ if (isValidConfig(parsed)) setCurrentConfig(parsed);
+ } catch {
+ // Invalid JSON
+ }
+ }
+ const storedSaved = safeGetItem(STORAGE_KEY_SAVED);
+ if (storedSaved) {
+ try {
+ const parsed = JSON.parse(storedSaved);
+ if (parsed && typeof parsed === 'object') setSavedConfigs(parsed);
+ } catch {
+ // Invalid JSON
+ }
+ }
+ const storedActive = safeGetItem(STORAGE_KEY_ACTIVE);
+ if (storedActive) setActiveConfigName(storedActive);
+ }, []);
+
+ const persistCurrent = useCallback((config: ConfigData) => {
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
+ saveTimeoutRef.current = setTimeout(() => {
+ safeSetItem(STORAGE_KEY_CURRENT, JSON.stringify(config));
+ }, 300);
+ }, []);
+
+ const updateCurrentConfig = useCallback(
+ (config: ConfigData) => {
+ setCurrentConfig(config);
+ setActiveConfigName(null);
+ if (mounted) {
+ persistCurrent(config);
+ safeRemoveItem(STORAGE_KEY_ACTIVE);
+ }
+ },
+ [mounted, persistCurrent]
+ );
+
+ const saveConfig = useCallback(
+ (name: string) => {
+ const newSaved = { ...savedConfigs, [name]: currentConfig };
+ setSavedConfigs(newSaved);
+ setActiveConfigName(name);
+ safeSetItem(STORAGE_KEY_SAVED, JSON.stringify(newSaved));
+ safeSetItem(STORAGE_KEY_ACTIVE, name);
+ },
+ [savedConfigs, currentConfig]
+ );
+
+ const loadConfig = useCallback(
+ (name: string) => {
+ const config = savedConfigs[name];
+ if (config) {
+ setCurrentConfig(config);
+ setActiveConfigName(name);
+ persistCurrent(config);
+ safeSetItem(STORAGE_KEY_ACTIVE, name);
+ }
+ },
+ [savedConfigs, persistCurrent]
+ );
+
+ const deleteConfig = useCallback(
+ (name: string) => {
+ const { [name]: _deleted, ...rest } = savedConfigs;
+ void _deleted;
+ setSavedConfigs(rest);
+ const clearActive = activeConfigName === name;
+ if (clearActive) setActiveConfigName(null);
+ safeSetItem(STORAGE_KEY_SAVED, JSON.stringify(rest));
+ if (clearActive) safeRemoveItem(STORAGE_KEY_ACTIVE);
+ },
+ [savedConfigs, activeConfigName]
+ );
+
+ const getConfigNames = useCallback(() => Object.keys(savedConfigs), [savedConfigs]);
+
+ return {
+ mounted,
+ currentConfig,
+ savedConfigs,
+ activeConfigName,
+ updateCurrentConfig,
+ saveConfig,
+ loadConfig,
+ deleteConfig,
+ getConfigNames,
+ };
+};
diff --git a/app/hooks/useErrorForm.ts b/app/hooks/useErrorForm.ts
index ded8e85..d43e5b4 100644
--- a/app/hooks/useErrorForm.ts
+++ b/app/hooks/useErrorForm.ts
@@ -124,6 +124,38 @@ export const useErrorForm = (
},
});
+ const getConfig = () => ({
+ dsn: form.dsn,
+ message: form.message,
+ priority: form.priority,
+ tags: form.tags,
+ errorCount: form.errorCount,
+ errorsToGenerate: form.errorsToGenerate,
+ fingerprintID: form.fingerprintID,
+ });
+
+ const loadConfig = (config: {
+ dsn: string;
+ message: string;
+ priority: Priority;
+ tags: CustomTag[];
+ errorCount: string;
+ errorsToGenerate: string;
+ fingerprintID: string;
+ }) => {
+ setForm((f) => ({
+ ...f,
+ dsn: config.dsn,
+ message: config.message,
+ priority: config.priority,
+ tags: config.tags,
+ errorCount: config.errorCount,
+ errorsToGenerate: config.errorsToGenerate,
+ fingerprintID: config.fingerprintID,
+ dsnError: '',
+ }));
+ };
+
return {
...form,
newTagKey,
@@ -137,5 +169,7 @@ export const useErrorForm = (
validate,
getPayload,
getPreviewPayload,
+ getConfig,
+ loadConfig,
};
};
diff --git a/app/styles/animations.ts b/app/styles/animations.ts
index c1818f8..0f67d47 100644
--- a/app/styles/animations.ts
+++ b/app/styles/animations.ts
@@ -76,3 +76,19 @@ export const shake: Variants = {
transition: { duration: 0.4 },
},
};
+
+export const slideInRight: Variants = {
+ hidden: { opacity: 0, x: 20 },
+ visible: {
+ opacity: 1,
+ x: 0,
+ transition: { staggerChildren: 0.05 },
+ },
+ exit: { opacity: 0, x: 20, transition: { duration: 0.15 } },
+};
+
+export const slideInRightItem: Variants = {
+ hidden: { opacity: 0, x: 10 },
+ visible: { opacity: 1, x: 0 },
+ exit: { opacity: 0, x: 10 },
+};
diff --git a/app/styles/globals.css b/app/styles/globals.css
index 2ebc151..3bf96c6 100644
--- a/app/styles/globals.css
+++ b/app/styles/globals.css
@@ -111,7 +111,7 @@ body {
}
.input-brutal {
- background-color: var(--color-brutal-black);
+ background-color: var(--color-bg-panel);
border: 3px solid var(--color-brutal-white);
color: var(--color-brutal-white);
transition:
@@ -135,7 +135,7 @@ body {
}
.select-brutal {
- background-color: var(--color-brutal-black);
+ background-color: var(--color-bg-panel);
border: 3px solid var(--color-brutal-white);
color: var(--color-brutal-white);
transition: