@startsimpli/hooks 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js DELETED
@@ -1,398 +0,0 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
- var reactQuery = require('@tanstack/react-query');
5
-
6
- // src/useTableFilters.ts
7
- function useTableFilters(initial) {
8
- const defaultFilters = {
9
- page: 1,
10
- pageSize: 20,
11
- ...initial
12
- };
13
- const [filters, setFilters] = react.useState(defaultFilters);
14
- const setFilter = react.useCallback((key, value) => {
15
- setFilters((prev) => ({ ...prev, [key]: value, page: 1 }));
16
- }, []);
17
- const setPage = react.useCallback((page) => {
18
- setFilters((prev) => ({ ...prev, page }));
19
- }, []);
20
- const setPageSize = react.useCallback((pageSize) => {
21
- setFilters((prev) => ({ ...prev, pageSize, page: 1 }));
22
- }, []);
23
- const setSearch = react.useCallback((search) => {
24
- setFilters((prev) => ({ ...prev, search, page: 1 }));
25
- }, []);
26
- const setSort = react.useCallback((field, direction) => {
27
- setFilters((prev) => ({ ...prev, sortField: field, sortDirection: direction }));
28
- }, []);
29
- const resetFilters = react.useCallback(() => {
30
- setFilters(defaultFilters);
31
- }, []);
32
- return {
33
- filters,
34
- setFilter,
35
- setPage,
36
- setPageSize,
37
- setSearch,
38
- setSort,
39
- resetFilters
40
- };
41
- }
42
-
43
- // src/filter-encoding.ts
44
- function encodeFilterConfig(config) {
45
- const json = JSON.stringify(config);
46
- return btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
47
- }
48
- function decodeFilterConfig(encoded) {
49
- try {
50
- const standardBase64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
51
- const padded = standardBase64 + "===".slice(0, (4 - standardBase64.length % 4) % 4);
52
- const decoded = atob(padded);
53
- return JSON.parse(decoded);
54
- } catch {
55
- return null;
56
- }
57
- }
58
- function parseUrlFilters(params) {
59
- const result = {};
60
- if (params.has("f")) result.f = params.get("f");
61
- if (params.has("s")) result.s = params.get("s");
62
- if (params.has("d")) result.d = params.get("d");
63
- if (params.has("p")) {
64
- const page = parseInt(params.get("p"), 10);
65
- if (!isNaN(page) && page > 0) result.p = page;
66
- }
67
- if (params.has("l")) {
68
- const limit = parseInt(params.get("l"), 10);
69
- if (!isNaN(limit) && limit > 0 && limit <= 1e3) result.l = limit;
70
- }
71
- return result;
72
- }
73
- function createSimpleFilter(field, operator, value) {
74
- return {
75
- groups: [{ logic: "AND", conditions: [{ field, operator, value }] }],
76
- globalLogic: "AND"
77
- };
78
- }
79
- function mergeFilters(...configs) {
80
- if (configs.length === 0) return { groups: [] };
81
- if (configs.length === 1) return configs[0];
82
- return {
83
- groups: configs.map((config) => ({
84
- logic: "AND",
85
- conditions: [],
86
- groups: config.groups
87
- })),
88
- globalLogic: "AND"
89
- };
90
- }
91
- function getFilterDescription(config) {
92
- if (config.groups.length === 0) return "No filters applied";
93
- function describeGroup(group) {
94
- const conditionDescs = group.conditions.map(
95
- (c) => `${c.field} ${c.operator} ${c.value}`
96
- );
97
- const groupDescs2 = group.groups?.map((g) => `(${describeGroup(g)})`) ?? [];
98
- return [...conditionDescs, ...groupDescs2].join(` ${group.logic} `);
99
- }
100
- const groupDescs = config.groups.map(describeGroup);
101
- return groupDescs.join(` ${config.globalLogic ?? "AND"} `);
102
- }
103
- function useCRUDMutation(mutationFn, opts) {
104
- const queryClient = reactQuery.useQueryClient();
105
- return reactQuery.useMutation({
106
- mutationFn,
107
- onSuccess: (data, variables) => {
108
- for (const key of opts.invalidateKeys) {
109
- queryClient.invalidateQueries({ queryKey: key });
110
- }
111
- opts.onSuccess?.(data, variables);
112
- },
113
- onError: opts.onError
114
- });
115
- }
116
- function useSavedViews({
117
- resource,
118
- loadFn,
119
- saveFn,
120
- updateFn,
121
- deleteFn
122
- }) {
123
- const [state, setState] = react.useState({
124
- views: [],
125
- currentViewId: null,
126
- loading: true,
127
- error: null
128
- });
129
- const fetchViews = react.useCallback(async () => {
130
- setState((prev) => ({ ...prev, loading: true, error: null }));
131
- try {
132
- const views = await loadFn(resource);
133
- const defaultView = views.find((v) => v.isDefault);
134
- setState((prev) => ({
135
- ...prev,
136
- views,
137
- currentViewId: defaultView?.id || null,
138
- loading: false
139
- }));
140
- } catch (error) {
141
- setState((prev) => ({
142
- ...prev,
143
- loading: false,
144
- error: error instanceof Error ? error.message : "Failed to load views"
145
- }));
146
- }
147
- }, [resource, loadFn]);
148
- react.useEffect(() => {
149
- fetchViews();
150
- }, [fetchViews]);
151
- const saveView = react.useCallback(
152
- async (viewData) => {
153
- const newView = await saveFn(resource, viewData);
154
- setState((prev) => ({
155
- ...prev,
156
- views: [...prev.views, newView],
157
- currentViewId: newView.id
158
- }));
159
- return newView;
160
- },
161
- [resource, saveFn]
162
- );
163
- const updateView = react.useCallback(
164
- async (viewId, updates) => {
165
- await updateFn(resource, viewId, updates);
166
- await fetchViews();
167
- },
168
- [resource, updateFn, fetchViews]
169
- );
170
- const deleteView = react.useCallback(
171
- async (viewId) => {
172
- await deleteFn(resource, viewId);
173
- setState((prev) => ({
174
- ...prev,
175
- views: prev.views.filter((v) => v.id !== viewId),
176
- currentViewId: prev.currentViewId === viewId ? null : prev.currentViewId
177
- }));
178
- },
179
- [resource, deleteFn]
180
- );
181
- const loadView = react.useCallback((viewId) => {
182
- setState((prev) => ({ ...prev, currentViewId: viewId }));
183
- }, []);
184
- const getCurrentView = react.useCallback(() => {
185
- return state.views.find((v) => v.id === state.currentViewId) || null;
186
- }, [state.views, state.currentViewId]);
187
- return {
188
- views: state.views,
189
- currentViewId: state.currentViewId,
190
- loading: state.loading,
191
- error: state.error,
192
- saveView,
193
- updateView,
194
- deleteView,
195
- loadView,
196
- getCurrentView,
197
- refreshViews: fetchViews
198
- };
199
- }
200
- function useRecentlyViewed(storageKey, maxItems = 5) {
201
- const [items, setItems] = react.useState([]);
202
- react.useEffect(() => {
203
- if (typeof window === "undefined") return;
204
- try {
205
- const raw = localStorage.getItem(storageKey);
206
- if (raw) {
207
- setItems(JSON.parse(raw));
208
- }
209
- } catch {
210
- }
211
- }, [storageKey]);
212
- const persist = react.useCallback(
213
- (updated) => {
214
- if (typeof window === "undefined") return;
215
- try {
216
- localStorage.setItem(storageKey, JSON.stringify(updated));
217
- } catch {
218
- }
219
- },
220
- [storageKey]
221
- );
222
- const trackView = react.useCallback(
223
- (item) => {
224
- setItems((prev) => {
225
- const deduped = prev.filter((entry) => entry.item.id !== item.id);
226
- const updated = [{ item, viewedAt: Date.now() }, ...deduped].slice(0, maxItems);
227
- persist(updated);
228
- return updated;
229
- });
230
- },
231
- [maxItems, persist]
232
- );
233
- const clear = react.useCallback(() => {
234
- setItems([]);
235
- if (typeof window === "undefined") return;
236
- try {
237
- localStorage.removeItem(storageKey);
238
- } catch {
239
- }
240
- }, [storageKey]);
241
- return {
242
- items: items.map((entry) => entry.item),
243
- timestamps: items.map((entry) => ({ id: entry.item.id, viewedAt: entry.viewedAt })),
244
- trackView,
245
- clear
246
- };
247
- }
248
- function useWizard(steps, initialStep) {
249
- const [currentStep, setCurrentStep] = react.useState(initialStep ?? steps[0]);
250
- const stepIndex = steps.indexOf(currentStep);
251
- const totalSteps = steps.length;
252
- const isFirstStep = stepIndex === 0;
253
- const isLastStep = stepIndex === totalSteps - 1;
254
- const goTo = react.useCallback(
255
- (step) => {
256
- if (steps.includes(step)) setCurrentStep(step);
257
- },
258
- [steps]
259
- );
260
- const next = react.useCallback(() => {
261
- if (!isLastStep) setCurrentStep(steps[stepIndex + 1]);
262
- }, [isLastStep, stepIndex, steps]);
263
- const prev = react.useCallback(() => {
264
- if (!isFirstStep) setCurrentStep(steps[stepIndex - 1]);
265
- }, [isFirstStep, stepIndex, steps]);
266
- const reset = react.useCallback(() => {
267
- setCurrentStep(initialStep ?? steps[0]);
268
- }, [initialStep, steps]);
269
- return {
270
- currentStep,
271
- stepIndex,
272
- totalSteps,
273
- isFirstStep,
274
- isLastStep,
275
- canGoBack: !isFirstStep,
276
- canGoNext: !isLastStep,
277
- goTo,
278
- next,
279
- prev,
280
- reset
281
- };
282
- }
283
- function useCSVImport({
284
- previewFn,
285
- importFn,
286
- onSuccess
287
- }) {
288
- const [step, setStep] = react.useState("upload");
289
- const [file, setFile] = react.useState(null);
290
- const [preview, setPreview] = react.useState(null);
291
- const [mappings, setMappings] = react.useState([]);
292
- const [result, setResult] = react.useState(null);
293
- const [isLoading, setIsLoading] = react.useState(false);
294
- const [error, setError] = react.useState(null);
295
- const handleFileSelect = react.useCallback(
296
- async (selectedFile) => {
297
- setFile(selectedFile);
298
- setError(null);
299
- setIsLoading(true);
300
- try {
301
- const previewData = await previewFn(selectedFile);
302
- setPreview(previewData);
303
- setMappings(previewData.suggested_mappings ?? []);
304
- setStep("mapping");
305
- } catch {
306
- setError("Failed to preview CSV file");
307
- } finally {
308
- setIsLoading(false);
309
- }
310
- },
311
- [previewFn]
312
- );
313
- const updateMapping = react.useCallback((csvColumn, targetField) => {
314
- setMappings((prev) => {
315
- const existing = prev.find((m) => m.csv_column === csvColumn);
316
- if (existing) {
317
- return prev.map(
318
- (m) => m.csv_column === csvColumn ? { ...m, target_field: targetField } : m
319
- );
320
- }
321
- return [...prev, { csv_column: csvColumn, target_field: targetField }];
322
- });
323
- }, []);
324
- const startImport = react.useCallback(async () => {
325
- if (!file) return;
326
- setStep("importing");
327
- setError(null);
328
- try {
329
- const importResult = await importFn(file, mappings);
330
- setResult(importResult);
331
- setStep("complete");
332
- onSuccess?.(importResult);
333
- } catch {
334
- setError("Failed to import CSV");
335
- setStep("mapping");
336
- }
337
- }, [file, mappings, importFn, onSuccess]);
338
- const reset = react.useCallback(() => {
339
- setStep("upload");
340
- setFile(null);
341
- setPreview(null);
342
- setMappings([]);
343
- setResult(null);
344
- setError(null);
345
- }, []);
346
- const goBack = react.useCallback(() => {
347
- if (step === "mapping") setStep("upload");
348
- }, [step]);
349
- return {
350
- step,
351
- file,
352
- preview,
353
- mappings,
354
- result,
355
- isLoading,
356
- error,
357
- handleFileSelect,
358
- updateMapping,
359
- startImport,
360
- reset,
361
- goBack
362
- };
363
- }
364
- function useCSVExport({ exportFn, filename = "export.csv" }) {
365
- const [isExporting, setIsExporting] = react.useState(false);
366
- const exportCSV = react.useCallback(async () => {
367
- setIsExporting(true);
368
- try {
369
- const data = await exportFn();
370
- const blob = typeof data === "string" ? new Blob([data], { type: "text/csv" }) : data;
371
- const url = URL.createObjectURL(blob);
372
- const a = document.createElement("a");
373
- a.href = url;
374
- a.download = filename;
375
- a.click();
376
- URL.revokeObjectURL(url);
377
- } finally {
378
- setIsExporting(false);
379
- }
380
- }, [exportFn, filename]);
381
- return { exportCSV, isExporting };
382
- }
383
-
384
- exports.createSimpleFilter = createSimpleFilter;
385
- exports.decodeFilterConfig = decodeFilterConfig;
386
- exports.encodeFilterConfig = encodeFilterConfig;
387
- exports.getFilterDescription = getFilterDescription;
388
- exports.mergeFilters = mergeFilters;
389
- exports.parseUrlFilters = parseUrlFilters;
390
- exports.useCRUDMutation = useCRUDMutation;
391
- exports.useCSVExport = useCSVExport;
392
- exports.useCSVImport = useCSVImport;
393
- exports.useRecentlyViewed = useRecentlyViewed;
394
- exports.useSavedViews = useSavedViews;
395
- exports.useTableFilters = useTableFilters;
396
- exports.useWizard = useWizard;
397
- //# sourceMappingURL=index.js.map
398
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useTableFilters.ts","../src/filter-encoding.ts","../src/useCRUDMutation.ts","../src/useSavedViews.ts","../src/useRecentlyViewed.ts","../src/useWizard.ts","../src/useCSV.ts"],"names":["useState","useCallback","groupDescs","useQueryClient","useMutation","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { useState, useCallback } from 'react'\n\ninterface TableFilterBase {\n page: number\n pageSize: number\n search?: string\n sortField?: string\n sortDirection?: 'asc' | 'desc'\n}\n\ntype TableFilters<TFilters extends Record<string, unknown>> = TFilters & TableFilterBase\n\ninterface UseTableFiltersReturn<TFilters extends Record<string, unknown>> {\n filters: TableFilters<TFilters>\n setFilter: <K extends keyof TFilters>(key: K, value: TFilters[K]) => void\n setPage: (page: number) => void\n setPageSize: (pageSize: number) => void\n setSearch: (search: string) => void\n setSort: (field: string, direction: 'asc' | 'desc') => void\n resetFilters: () => void\n}\n\nexport function useTableFilters<TFilters extends Record<string, unknown>>(\n initial: TFilters & { page?: number; pageSize?: number; sortField?: string; sortDirection?: 'asc' | 'desc' }\n): UseTableFiltersReturn<TFilters> {\n const defaultFilters: TableFilters<TFilters> = {\n page: 1,\n pageSize: 20,\n ...initial,\n } as TableFilters<TFilters>\n\n const [filters, setFilters] = useState<TableFilters<TFilters>>(defaultFilters)\n\n const setFilter = useCallback(<K extends keyof TFilters>(key: K, value: TFilters[K]) => {\n setFilters(prev => ({ ...prev, [key]: value, page: 1 }))\n }, [])\n\n const setPage = useCallback((page: number) => {\n setFilters(prev => ({ ...prev, page }))\n }, [])\n\n const setPageSize = useCallback((pageSize: number) => {\n setFilters(prev => ({ ...prev, pageSize, page: 1 }))\n }, [])\n\n const setSearch = useCallback((search: string) => {\n setFilters(prev => ({ ...prev, search, page: 1 }))\n }, [])\n\n const setSort = useCallback((field: string, direction: 'asc' | 'desc') => {\n setFilters(prev => ({ ...prev, sortField: field, sortDirection: direction }))\n }, [])\n\n const resetFilters = useCallback(() => {\n setFilters(defaultFilters)\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n return {\n filters,\n setFilter,\n setPage,\n setPageSize,\n setSearch,\n setSort,\n resetFilters,\n }\n}\n","/**\n * URL filter encoding utilities.\n *\n * Provides base64 encode/decode for FilterConfig (so filter state can be\n * stored in URL query params as compact strings) plus helpers for common\n * filter operations.\n *\n * These are pure functions with no React or Next.js dependencies.\n */\n\nimport type {\n FilterConfig,\n FilterCondition,\n FilterGroup,\n FilterOperator,\n FilterValue,\n EncodedFilterState,\n} from './filter-types'\n\n/**\n * Encode a FilterConfig to a URL-safe base64 string.\n */\nexport function encodeFilterConfig(config: FilterConfig): string {\n const json = JSON.stringify(config)\n return btoa(json).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\n/**\n * Decode a URL-safe base64 string back to a FilterConfig.\n * Returns null if the string is invalid or cannot be parsed.\n */\nexport function decodeFilterConfig(encoded: string): FilterConfig | null {\n try {\n // Convert URL-safe base64 back to standard base64\n const standardBase64 = encoded.replace(/-/g, '+').replace(/_/g, '/')\n // Add padding if needed\n const padded = standardBase64 + '==='.slice(0, (4 - standardBase64.length % 4) % 4)\n const decoded = atob(padded)\n return JSON.parse(decoded) as FilterConfig\n } catch {\n return null\n }\n}\n\n/**\n * Parse URLSearchParams into an EncodedFilterState.\n */\nexport function parseUrlFilters(params: URLSearchParams): EncodedFilterState {\n const result: EncodedFilterState = {}\n\n if (params.has('f')) result.f = params.get('f')!\n if (params.has('s')) result.s = params.get('s')!\n if (params.has('d')) result.d = params.get('d')!\n\n if (params.has('p')) {\n const page = parseInt(params.get('p')!, 10)\n if (!isNaN(page) && page > 0) result.p = page\n }\n\n if (params.has('l')) {\n const limit = parseInt(params.get('l')!, 10)\n if (!isNaN(limit) && limit > 0 && limit <= 1000) result.l = limit\n }\n\n return result\n}\n\n/**\n * Build a single-condition FilterConfig.\n */\nexport function createSimpleFilter(\n field: string,\n operator: FilterOperator,\n value: FilterValue\n): FilterConfig {\n return {\n groups: [{ logic: 'AND', conditions: [{ field, operator, value }] }],\n globalLogic: 'AND',\n }\n}\n\n/**\n * Merge multiple FilterConfigs with AND logic.\n */\nexport function mergeFilters(...configs: FilterConfig[]): FilterConfig {\n if (configs.length === 0) return { groups: [] }\n if (configs.length === 1) return configs[0]\n\n return {\n groups: configs.map(config => ({\n logic: 'AND' as const,\n conditions: [] as FilterCondition[],\n groups: config.groups,\n })),\n globalLogic: 'AND',\n }\n}\n\n/**\n * Get a human-readable description of a FilterConfig.\n */\nexport function getFilterDescription(config: FilterConfig): string {\n if (config.groups.length === 0) return 'No filters applied'\n\n function describeGroup(group: FilterGroup): string {\n const conditionDescs = group.conditions.map(\n c => `${c.field} ${c.operator} ${c.value}`\n )\n const groupDescs = group.groups?.map(g => `(${describeGroup(g)})`) ?? []\n return [...conditionDescs, ...groupDescs].join(` ${group.logic} `)\n }\n\n const groupDescs = config.groups.map(describeGroup)\n return groupDescs.join(` ${config.globalLogic ?? 'AND'} `)\n}\n","import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport type { QueryKey, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'\n\ninterface UseCRUDMutationOptions<TInput, TOutput> {\n invalidateKeys: QueryKey[]\n onSuccess?: (data: TOutput, variables: TInput) => void\n onError?: (error: Error, variables: TInput) => void\n}\n\nexport function useCRUDMutation<TInput, TOutput>(\n mutationFn: (input: TInput) => Promise<TOutput>,\n opts: UseCRUDMutationOptions<TInput, TOutput>\n): UseMutationResult<TOutput, Error, TInput> {\n const queryClient = useQueryClient()\n\n return useMutation({\n mutationFn,\n onSuccess: (data, variables) => {\n for (const key of opts.invalidateKeys) {\n queryClient.invalidateQueries({ queryKey: key })\n }\n opts.onSuccess?.(data, variables)\n },\n onError: opts.onError,\n })\n}\n","import { useState, useEffect, useCallback } from 'react'\n\nexport interface SavedView {\n id: string\n name: string\n isDefault?: boolean\n createdAt?: string\n [key: string]: unknown\n}\n\ninterface SavedViewsState<T extends SavedView> {\n views: T[]\n currentViewId: string | null\n loading: boolean\n error: string | null\n}\n\ninterface UseSavedViewsOptions<T extends SavedView> {\n resource: string\n loadFn: (resource: string) => Promise<T[]>\n saveFn: (resource: string, view: Omit<T, 'id' | 'createdAt'>) => Promise<T>\n updateFn: (resource: string, viewId: string, updates: Partial<T>) => Promise<T>\n deleteFn: (resource: string, viewId: string) => Promise<void>\n}\n\nexport function useSavedViews<T extends SavedView>({\n resource,\n loadFn,\n saveFn,\n updateFn,\n deleteFn,\n}: UseSavedViewsOptions<T>) {\n const [state, setState] = useState<SavedViewsState<T>>({\n views: [],\n currentViewId: null,\n loading: true,\n error: null,\n })\n\n const fetchViews = useCallback(async () => {\n setState(prev => ({ ...prev, loading: true, error: null }))\n try {\n const views = await loadFn(resource)\n const defaultView = views.find(v => v.isDefault)\n setState(prev => ({\n ...prev,\n views,\n currentViewId: defaultView?.id || null,\n loading: false,\n }))\n } catch (error) {\n setState(prev => ({\n ...prev,\n loading: false,\n error: error instanceof Error ? error.message : 'Failed to load views',\n }))\n }\n }, [resource, loadFn])\n\n useEffect(() => {\n fetchViews()\n }, [fetchViews])\n\n const saveView = useCallback(\n async (viewData: Omit<T, 'id' | 'createdAt'>): Promise<T> => {\n const newView = await saveFn(resource, viewData)\n setState(prev => ({\n ...prev,\n views: [...prev.views, newView],\n currentViewId: newView.id,\n }))\n return newView\n },\n [resource, saveFn]\n )\n\n const updateView = useCallback(\n async (viewId: string, updates: Partial<T>): Promise<void> => {\n await updateFn(resource, viewId, updates)\n await fetchViews()\n },\n [resource, updateFn, fetchViews]\n )\n\n const deleteView = useCallback(\n async (viewId: string): Promise<void> => {\n await deleteFn(resource, viewId)\n setState(prev => ({\n ...prev,\n views: prev.views.filter(v => v.id !== viewId),\n currentViewId: prev.currentViewId === viewId ? null : prev.currentViewId,\n }))\n },\n [resource, deleteFn]\n )\n\n const loadView = useCallback((viewId: string) => {\n setState(prev => ({ ...prev, currentViewId: viewId }))\n }, [])\n\n const getCurrentView = useCallback((): T | null => {\n return state.views.find(v => v.id === state.currentViewId) || null\n }, [state.views, state.currentViewId])\n\n return {\n views: state.views,\n currentViewId: state.currentViewId,\n loading: state.loading,\n error: state.error,\n saveView,\n updateView,\n deleteView,\n loadView,\n getCurrentView,\n refreshViews: fetchViews,\n }\n}\n","import { useState, useCallback, useEffect } from 'react'\n\ninterface StoredItem<T> {\n item: T\n viewedAt: number\n}\n\nexport function useRecentlyViewed<T extends { id: string }>(\n storageKey: string,\n maxItems: number = 5\n) {\n const [items, setItems] = useState<StoredItem<T>[]>([])\n\n // Load from localStorage on mount\n useEffect(() => {\n if (typeof window === 'undefined') return\n try {\n const raw = localStorage.getItem(storageKey)\n if (raw) {\n setItems(JSON.parse(raw) as StoredItem<T>[])\n }\n } catch {\n // Corrupted storage, start fresh\n }\n }, [storageKey])\n\n const persist = useCallback(\n (updated: StoredItem<T>[]) => {\n if (typeof window === 'undefined') return\n try {\n localStorage.setItem(storageKey, JSON.stringify(updated))\n } catch {\n // Storage quota exceeded\n }\n },\n [storageKey]\n )\n\n const trackView = useCallback(\n (item: T) => {\n setItems(prev => {\n const deduped = prev.filter(entry => entry.item.id !== item.id)\n const updated = [{ item, viewedAt: Date.now() }, ...deduped].slice(0, maxItems)\n persist(updated)\n return updated\n })\n },\n [maxItems, persist]\n )\n\n const clear = useCallback(() => {\n setItems([])\n if (typeof window === 'undefined') return\n try {\n localStorage.removeItem(storageKey)\n } catch {\n // ignore\n }\n }, [storageKey])\n\n return {\n items: items.map(entry => entry.item),\n timestamps: items.map(entry => ({ id: entry.item.id, viewedAt: entry.viewedAt })),\n trackView,\n clear,\n }\n}\n","import { useState, useCallback } from 'react'\n\nexport interface WizardState<TStep extends string> {\n currentStep: TStep\n stepIndex: number\n totalSteps: number\n isFirstStep: boolean\n isLastStep: boolean\n canGoBack: boolean\n canGoNext: boolean\n goTo: (step: TStep) => void\n next: () => void\n prev: () => void\n reset: () => void\n}\n\nexport function useWizard<TStep extends string>(\n steps: readonly TStep[],\n initialStep?: TStep\n): WizardState<TStep> {\n const [currentStep, setCurrentStep] = useState<TStep>(initialStep ?? steps[0])\n\n const stepIndex = steps.indexOf(currentStep)\n const totalSteps = steps.length\n const isFirstStep = stepIndex === 0\n const isLastStep = stepIndex === totalSteps - 1\n\n const goTo = useCallback(\n (step: TStep) => {\n if (steps.includes(step)) setCurrentStep(step)\n },\n [steps]\n )\n\n const next = useCallback(() => {\n if (!isLastStep) setCurrentStep(steps[stepIndex + 1])\n }, [isLastStep, stepIndex, steps])\n\n const prev = useCallback(() => {\n if (!isFirstStep) setCurrentStep(steps[stepIndex - 1])\n }, [isFirstStep, stepIndex, steps])\n\n const reset = useCallback(() => {\n setCurrentStep(initialStep ?? steps[0])\n }, [initialStep, steps])\n\n return {\n currentStep,\n stepIndex,\n totalSteps,\n isFirstStep,\n isLastStep,\n canGoBack: !isFirstStep,\n canGoNext: !isLastStep,\n goTo,\n next,\n prev,\n reset,\n }\n}\n","import { useState, useCallback } from 'react'\n\nexport interface CSVColumnMapping {\n csv_column: string\n target_field: string\n}\n\nexport interface CSVPreviewResult<TField extends string = string> {\n columns: string[]\n sample_rows: Record<string, string>[]\n suggested_mappings?: CSVColumnMapping[]\n total_rows?: number\n}\n\nexport interface CSVImportResult {\n total_rows: number\n successful: number\n failed: number\n errors: Array<{ row: number; message: string }>\n}\n\nexport type CSVImportStep = 'upload' | 'mapping' | 'importing' | 'complete'\n\nexport interface UseCSVImportOptions<TField extends string = string> {\n previewFn: (file: File) => Promise<CSVPreviewResult<TField>>\n importFn: (file: File, mappings: CSVColumnMapping[]) => Promise<CSVImportResult>\n onSuccess?: (result: CSVImportResult) => void\n}\n\nexport interface UseCSVImportState {\n step: CSVImportStep\n file: File | null\n preview: CSVPreviewResult | null\n mappings: CSVColumnMapping[]\n result: CSVImportResult | null\n isLoading: boolean\n error: string | null\n handleFileSelect: (file: File) => Promise<void>\n updateMapping: (csvColumn: string, targetField: string) => void\n startImport: () => Promise<void>\n reset: () => void\n goBack: () => void\n}\n\nexport function useCSVImport<TField extends string = string>({\n previewFn,\n importFn,\n onSuccess,\n}: UseCSVImportOptions<TField>): UseCSVImportState {\n const [step, setStep] = useState<CSVImportStep>('upload')\n const [file, setFile] = useState<File | null>(null)\n const [preview, setPreview] = useState<CSVPreviewResult | null>(null)\n const [mappings, setMappings] = useState<CSVColumnMapping[]>([])\n const [result, setResult] = useState<CSVImportResult | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleFileSelect = useCallback(\n async (selectedFile: File) => {\n setFile(selectedFile)\n setError(null)\n setIsLoading(true)\n try {\n const previewData = await previewFn(selectedFile)\n setPreview(previewData)\n setMappings(previewData.suggested_mappings ?? [])\n setStep('mapping')\n } catch {\n setError('Failed to preview CSV file')\n } finally {\n setIsLoading(false)\n }\n },\n [previewFn]\n )\n\n const updateMapping = useCallback((csvColumn: string, targetField: string) => {\n setMappings((prev) => {\n const existing = prev.find((m) => m.csv_column === csvColumn)\n if (existing) {\n return prev.map((m) =>\n m.csv_column === csvColumn ? { ...m, target_field: targetField } : m\n )\n }\n return [...prev, { csv_column: csvColumn, target_field: targetField }]\n })\n }, [])\n\n const startImport = useCallback(async () => {\n if (!file) return\n setStep('importing')\n setError(null)\n try {\n const importResult = await importFn(file, mappings)\n setResult(importResult)\n setStep('complete')\n onSuccess?.(importResult)\n } catch {\n setError('Failed to import CSV')\n setStep('mapping')\n }\n }, [file, mappings, importFn, onSuccess])\n\n const reset = useCallback(() => {\n setStep('upload')\n setFile(null)\n setPreview(null)\n setMappings([])\n setResult(null)\n setError(null)\n }, [])\n\n const goBack = useCallback(() => {\n if (step === 'mapping') setStep('upload')\n }, [step])\n\n return {\n step,\n file,\n preview,\n mappings,\n result,\n isLoading,\n error,\n handleFileSelect,\n updateMapping,\n startImport,\n reset,\n goBack,\n }\n}\n\nexport interface UseCSVExportOptions {\n exportFn: () => Promise<Blob | string>\n filename?: string\n}\n\nexport function useCSVExport({ exportFn, filename = 'export.csv' }: UseCSVExportOptions) {\n const [isExporting, setIsExporting] = useState(false)\n\n const exportCSV = useCallback(async () => {\n setIsExporting(true)\n try {\n const data = await exportFn()\n const blob = typeof data === 'string' ? new Blob([data], { type: 'text/csv' }) : data\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n } finally {\n setIsExporting(false)\n }\n }, [exportFn, filename])\n\n return { exportCSV, isExporting }\n}\n"]}