@startsimpli/hooks 0.1.1

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.mjs ADDED
@@ -0,0 +1,384 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { useQueryClient, useMutation } from '@tanstack/react-query';
3
+
4
+ // src/useTableFilters.ts
5
+ function useTableFilters(initial) {
6
+ const defaultFilters = {
7
+ page: 1,
8
+ pageSize: 20,
9
+ ...initial
10
+ };
11
+ const [filters, setFilters] = useState(defaultFilters);
12
+ const setFilter = useCallback((key, value) => {
13
+ setFilters((prev) => ({ ...prev, [key]: value, page: 1 }));
14
+ }, []);
15
+ const setPage = useCallback((page) => {
16
+ setFilters((prev) => ({ ...prev, page }));
17
+ }, []);
18
+ const setPageSize = useCallback((pageSize) => {
19
+ setFilters((prev) => ({ ...prev, pageSize, page: 1 }));
20
+ }, []);
21
+ const setSearch = useCallback((search) => {
22
+ setFilters((prev) => ({ ...prev, search, page: 1 }));
23
+ }, []);
24
+ const setSort = useCallback((field, direction) => {
25
+ setFilters((prev) => ({ ...prev, sortField: field, sortDirection: direction }));
26
+ }, []);
27
+ const resetFilters = useCallback(() => {
28
+ setFilters(defaultFilters);
29
+ }, []);
30
+ return {
31
+ filters,
32
+ setFilter,
33
+ setPage,
34
+ setPageSize,
35
+ setSearch,
36
+ setSort,
37
+ resetFilters
38
+ };
39
+ }
40
+
41
+ // src/filter-encoding.ts
42
+ function encodeFilterConfig(config) {
43
+ const json = JSON.stringify(config);
44
+ return btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
45
+ }
46
+ function decodeFilterConfig(encoded) {
47
+ try {
48
+ const standardBase64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
49
+ const padded = standardBase64 + "===".slice(0, (4 - standardBase64.length % 4) % 4);
50
+ const decoded = atob(padded);
51
+ return JSON.parse(decoded);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function parseUrlFilters(params) {
57
+ const result = {};
58
+ if (params.has("f")) result.f = params.get("f");
59
+ if (params.has("s")) result.s = params.get("s");
60
+ if (params.has("d")) result.d = params.get("d");
61
+ if (params.has("p")) {
62
+ const page = parseInt(params.get("p"), 10);
63
+ if (!isNaN(page) && page > 0) result.p = page;
64
+ }
65
+ if (params.has("l")) {
66
+ const limit = parseInt(params.get("l"), 10);
67
+ if (!isNaN(limit) && limit > 0 && limit <= 1e3) result.l = limit;
68
+ }
69
+ return result;
70
+ }
71
+ function createSimpleFilter(field, operator, value) {
72
+ return {
73
+ groups: [{ logic: "AND", conditions: [{ field, operator, value }] }],
74
+ globalLogic: "AND"
75
+ };
76
+ }
77
+ function mergeFilters(...configs) {
78
+ if (configs.length === 0) return { groups: [] };
79
+ if (configs.length === 1) return configs[0];
80
+ return {
81
+ groups: configs.map((config) => ({
82
+ logic: "AND",
83
+ conditions: [],
84
+ groups: config.groups
85
+ })),
86
+ globalLogic: "AND"
87
+ };
88
+ }
89
+ function getFilterDescription(config) {
90
+ if (config.groups.length === 0) return "No filters applied";
91
+ function describeGroup(group) {
92
+ const conditionDescs = group.conditions.map(
93
+ (c) => `${c.field} ${c.operator} ${c.value}`
94
+ );
95
+ const groupDescs2 = group.groups?.map((g) => `(${describeGroup(g)})`) ?? [];
96
+ return [...conditionDescs, ...groupDescs2].join(` ${group.logic} `);
97
+ }
98
+ const groupDescs = config.groups.map(describeGroup);
99
+ return groupDescs.join(` ${config.globalLogic ?? "AND"} `);
100
+ }
101
+ function useCRUDMutation(mutationFn, opts) {
102
+ const queryClient = useQueryClient();
103
+ return useMutation({
104
+ mutationFn,
105
+ onSuccess: (data, variables) => {
106
+ for (const key of opts.invalidateKeys) {
107
+ queryClient.invalidateQueries({ queryKey: key });
108
+ }
109
+ opts.onSuccess?.(data, variables);
110
+ },
111
+ onError: opts.onError
112
+ });
113
+ }
114
+ function useSavedViews({
115
+ resource,
116
+ loadFn,
117
+ saveFn,
118
+ updateFn,
119
+ deleteFn
120
+ }) {
121
+ const [state, setState] = useState({
122
+ views: [],
123
+ currentViewId: null,
124
+ loading: true,
125
+ error: null
126
+ });
127
+ const fetchViews = useCallback(async () => {
128
+ setState((prev) => ({ ...prev, loading: true, error: null }));
129
+ try {
130
+ const views = await loadFn(resource);
131
+ const defaultView = views.find((v) => v.isDefault);
132
+ setState((prev) => ({
133
+ ...prev,
134
+ views,
135
+ currentViewId: defaultView?.id || null,
136
+ loading: false
137
+ }));
138
+ } catch (error) {
139
+ setState((prev) => ({
140
+ ...prev,
141
+ loading: false,
142
+ error: error instanceof Error ? error.message : "Failed to load views"
143
+ }));
144
+ }
145
+ }, [resource, loadFn]);
146
+ useEffect(() => {
147
+ fetchViews();
148
+ }, [fetchViews]);
149
+ const saveView = useCallback(
150
+ async (viewData) => {
151
+ const newView = await saveFn(resource, viewData);
152
+ setState((prev) => ({
153
+ ...prev,
154
+ views: [...prev.views, newView],
155
+ currentViewId: newView.id
156
+ }));
157
+ return newView;
158
+ },
159
+ [resource, saveFn]
160
+ );
161
+ const updateView = useCallback(
162
+ async (viewId, updates) => {
163
+ await updateFn(resource, viewId, updates);
164
+ await fetchViews();
165
+ },
166
+ [resource, updateFn, fetchViews]
167
+ );
168
+ const deleteView = useCallback(
169
+ async (viewId) => {
170
+ await deleteFn(resource, viewId);
171
+ setState((prev) => ({
172
+ ...prev,
173
+ views: prev.views.filter((v) => v.id !== viewId),
174
+ currentViewId: prev.currentViewId === viewId ? null : prev.currentViewId
175
+ }));
176
+ },
177
+ [resource, deleteFn]
178
+ );
179
+ const loadView = useCallback((viewId) => {
180
+ setState((prev) => ({ ...prev, currentViewId: viewId }));
181
+ }, []);
182
+ const getCurrentView = useCallback(() => {
183
+ return state.views.find((v) => v.id === state.currentViewId) || null;
184
+ }, [state.views, state.currentViewId]);
185
+ return {
186
+ views: state.views,
187
+ currentViewId: state.currentViewId,
188
+ loading: state.loading,
189
+ error: state.error,
190
+ saveView,
191
+ updateView,
192
+ deleteView,
193
+ loadView,
194
+ getCurrentView,
195
+ refreshViews: fetchViews
196
+ };
197
+ }
198
+ function useRecentlyViewed(storageKey, maxItems = 5) {
199
+ const [items, setItems] = useState([]);
200
+ useEffect(() => {
201
+ if (typeof window === "undefined") return;
202
+ try {
203
+ const raw = localStorage.getItem(storageKey);
204
+ if (raw) {
205
+ setItems(JSON.parse(raw));
206
+ }
207
+ } catch {
208
+ }
209
+ }, [storageKey]);
210
+ const persist = useCallback(
211
+ (updated) => {
212
+ if (typeof window === "undefined") return;
213
+ try {
214
+ localStorage.setItem(storageKey, JSON.stringify(updated));
215
+ } catch {
216
+ }
217
+ },
218
+ [storageKey]
219
+ );
220
+ const trackView = useCallback(
221
+ (item) => {
222
+ setItems((prev) => {
223
+ const deduped = prev.filter((entry) => entry.item.id !== item.id);
224
+ const updated = [{ item, viewedAt: Date.now() }, ...deduped].slice(0, maxItems);
225
+ persist(updated);
226
+ return updated;
227
+ });
228
+ },
229
+ [maxItems, persist]
230
+ );
231
+ const clear = useCallback(() => {
232
+ setItems([]);
233
+ if (typeof window === "undefined") return;
234
+ try {
235
+ localStorage.removeItem(storageKey);
236
+ } catch {
237
+ }
238
+ }, [storageKey]);
239
+ return {
240
+ items: items.map((entry) => entry.item),
241
+ timestamps: items.map((entry) => ({ id: entry.item.id, viewedAt: entry.viewedAt })),
242
+ trackView,
243
+ clear
244
+ };
245
+ }
246
+ function useWizard(steps, initialStep) {
247
+ const [currentStep, setCurrentStep] = useState(initialStep ?? steps[0]);
248
+ const stepIndex = steps.indexOf(currentStep);
249
+ const totalSteps = steps.length;
250
+ const isFirstStep = stepIndex === 0;
251
+ const isLastStep = stepIndex === totalSteps - 1;
252
+ const goTo = useCallback(
253
+ (step) => {
254
+ if (steps.includes(step)) setCurrentStep(step);
255
+ },
256
+ [steps]
257
+ );
258
+ const next = useCallback(() => {
259
+ if (!isLastStep) setCurrentStep(steps[stepIndex + 1]);
260
+ }, [isLastStep, stepIndex, steps]);
261
+ const prev = useCallback(() => {
262
+ if (!isFirstStep) setCurrentStep(steps[stepIndex - 1]);
263
+ }, [isFirstStep, stepIndex, steps]);
264
+ const reset = useCallback(() => {
265
+ setCurrentStep(initialStep ?? steps[0]);
266
+ }, [initialStep, steps]);
267
+ return {
268
+ currentStep,
269
+ stepIndex,
270
+ totalSteps,
271
+ isFirstStep,
272
+ isLastStep,
273
+ canGoBack: !isFirstStep,
274
+ canGoNext: !isLastStep,
275
+ goTo,
276
+ next,
277
+ prev,
278
+ reset
279
+ };
280
+ }
281
+ function useCSVImport({
282
+ previewFn,
283
+ importFn,
284
+ onSuccess
285
+ }) {
286
+ const [step, setStep] = useState("upload");
287
+ const [file, setFile] = useState(null);
288
+ const [preview, setPreview] = useState(null);
289
+ const [mappings, setMappings] = useState([]);
290
+ const [result, setResult] = useState(null);
291
+ const [isLoading, setIsLoading] = useState(false);
292
+ const [error, setError] = useState(null);
293
+ const handleFileSelect = useCallback(
294
+ async (selectedFile) => {
295
+ setFile(selectedFile);
296
+ setError(null);
297
+ setIsLoading(true);
298
+ try {
299
+ const previewData = await previewFn(selectedFile);
300
+ setPreview(previewData);
301
+ setMappings(previewData.suggested_mappings ?? []);
302
+ setStep("mapping");
303
+ } catch {
304
+ setError("Failed to preview CSV file");
305
+ } finally {
306
+ setIsLoading(false);
307
+ }
308
+ },
309
+ [previewFn]
310
+ );
311
+ const updateMapping = useCallback((csvColumn, targetField) => {
312
+ setMappings((prev) => {
313
+ const existing = prev.find((m) => m.csv_column === csvColumn);
314
+ if (existing) {
315
+ return prev.map(
316
+ (m) => m.csv_column === csvColumn ? { ...m, target_field: targetField } : m
317
+ );
318
+ }
319
+ return [...prev, { csv_column: csvColumn, target_field: targetField }];
320
+ });
321
+ }, []);
322
+ const startImport = useCallback(async () => {
323
+ if (!file) return;
324
+ setStep("importing");
325
+ setError(null);
326
+ try {
327
+ const importResult = await importFn(file, mappings);
328
+ setResult(importResult);
329
+ setStep("complete");
330
+ onSuccess?.(importResult);
331
+ } catch {
332
+ setError("Failed to import CSV");
333
+ setStep("mapping");
334
+ }
335
+ }, [file, mappings, importFn, onSuccess]);
336
+ const reset = useCallback(() => {
337
+ setStep("upload");
338
+ setFile(null);
339
+ setPreview(null);
340
+ setMappings([]);
341
+ setResult(null);
342
+ setError(null);
343
+ }, []);
344
+ const goBack = useCallback(() => {
345
+ if (step === "mapping") setStep("upload");
346
+ }, [step]);
347
+ return {
348
+ step,
349
+ file,
350
+ preview,
351
+ mappings,
352
+ result,
353
+ isLoading,
354
+ error,
355
+ handleFileSelect,
356
+ updateMapping,
357
+ startImport,
358
+ reset,
359
+ goBack
360
+ };
361
+ }
362
+ function useCSVExport({ exportFn, filename = "export.csv" }) {
363
+ const [isExporting, setIsExporting] = useState(false);
364
+ const exportCSV = useCallback(async () => {
365
+ setIsExporting(true);
366
+ try {
367
+ const data = await exportFn();
368
+ const blob = typeof data === "string" ? new Blob([data], { type: "text/csv" }) : data;
369
+ const url = URL.createObjectURL(blob);
370
+ const a = document.createElement("a");
371
+ a.href = url;
372
+ a.download = filename;
373
+ a.click();
374
+ URL.revokeObjectURL(url);
375
+ } finally {
376
+ setIsExporting(false);
377
+ }
378
+ }, [exportFn, filename]);
379
+ return { exportCSV, isExporting };
380
+ }
381
+
382
+ export { createSimpleFilter, decodeFilterConfig, encodeFilterConfig, getFilterDescription, mergeFilters, parseUrlFilters, useCRUDMutation, useCSVExport, useCSVImport, useRecentlyViewed, useSavedViews, useTableFilters, useWizard };
383
+ //# sourceMappingURL=index.mjs.map
384
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
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":["groupDescs","useState","useCallback","useEffect"],"mappings":"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","file":"index.mjs","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"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@startsimpli/hooks",
3
+ "version": "0.1.1",
4
+ "description": "Shared React hooks for StartSimpli apps",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "type-check": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0",
32
+ "@tanstack/react-query": ">=5.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@tanstack/react-query": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@tanstack/react-query": "^5.0.0",
41
+ "@testing-library/react": "^14.0.0",
42
+ "@types/node": "^20.11.0",
43
+ "@types/react": "^18.2.0",
44
+ "react": "^18.2.0",
45
+ "react-dom": "^18.2.0",
46
+ "tsup": "^8.5.1",
47
+ "typescript": "^5.3.3",
48
+ "vitest": "^1.2.0"
49
+ },
50
+ "module": "./dist/index.mjs"
51
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { renderHook, act, waitFor } from '@testing-library/react'
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import React from 'react'
5
+ import { useCRUDMutation } from '../useCRUDMutation'
6
+
7
+ function createWrapper() {
8
+ const queryClient = new QueryClient({
9
+ defaultOptions: { mutations: { retry: false }, queries: { retry: false } },
10
+ })
11
+ return function Wrapper({ children }: { children: React.ReactNode }) {
12
+ return React.createElement(QueryClientProvider, { client: queryClient }, children)
13
+ }
14
+ }
15
+
16
+ describe('useCRUDMutation', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks()
19
+ })
20
+
21
+ it('calls mutationFn on mutate', async () => {
22
+ const mutationFn = vi.fn().mockResolvedValue({ id: '1', name: 'Created' })
23
+ const wrapper = createWrapper()
24
+
25
+ const { result } = renderHook(
26
+ () => useCRUDMutation(mutationFn, { invalidateKeys: [] }),
27
+ { wrapper }
28
+ )
29
+
30
+ await act(async () => {
31
+ result.current.mutate({ name: 'New Item' })
32
+ })
33
+
34
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
35
+
36
+ // react-query v5 passes a context object as the second arg to mutationFn
37
+ expect(mutationFn).toHaveBeenCalledWith({ name: 'New Item' }, expect.any(Object))
38
+ })
39
+
40
+ it('calls onSuccess callback after successful mutation', async () => {
41
+ const onSuccess = vi.fn()
42
+ const mutationFn = vi.fn().mockResolvedValue({ id: '1' })
43
+ const wrapper = createWrapper()
44
+
45
+ const { result } = renderHook(
46
+ () => useCRUDMutation(mutationFn, { invalidateKeys: [], onSuccess }),
47
+ { wrapper }
48
+ )
49
+
50
+ await act(async () => {
51
+ result.current.mutate({ name: 'Test' })
52
+ })
53
+
54
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
55
+
56
+ expect(onSuccess).toHaveBeenCalledWith({ id: '1' }, { name: 'Test' })
57
+ })
58
+
59
+ it('calls onError callback when mutation fails', async () => {
60
+ const onError = vi.fn()
61
+ const mutationFn = vi.fn().mockRejectedValue(new Error('Server error'))
62
+ const wrapper = createWrapper()
63
+
64
+ const { result } = renderHook(
65
+ () => useCRUDMutation(mutationFn, { invalidateKeys: [], onError }),
66
+ { wrapper }
67
+ )
68
+
69
+ await act(async () => {
70
+ result.current.mutate({})
71
+ })
72
+
73
+ await waitFor(() => expect(result.current.isError).toBe(true))
74
+
75
+ // opts.onError is passed directly to useMutation, react-query v5 calls it with (error, variables, context, mutation)
76
+ expect(onError).toHaveBeenCalled()
77
+ const [firstArg] = onError.mock.calls[0]
78
+ expect(firstArg).toBeInstanceOf(Error)
79
+ expect(firstArg.message).toBe('Server error')
80
+ })
81
+
82
+ it('sets isError to true when mutation fails', async () => {
83
+ const mutationFn = vi.fn().mockRejectedValue(new Error('Failure'))
84
+ const wrapper = createWrapper()
85
+
86
+ const { result } = renderHook(
87
+ () => useCRUDMutation(mutationFn, { invalidateKeys: [] }),
88
+ { wrapper }
89
+ )
90
+
91
+ await act(async () => {
92
+ result.current.mutate({})
93
+ })
94
+
95
+ await waitFor(() => expect(result.current.isError).toBe(true))
96
+ })
97
+
98
+ it('exposes data from a successful mutation', async () => {
99
+ const responseData = { id: '42', name: 'Result' }
100
+ const mutationFn = vi.fn().mockResolvedValue(responseData)
101
+ const wrapper = createWrapper()
102
+
103
+ const { result } = renderHook(
104
+ () => useCRUDMutation(mutationFn, { invalidateKeys: [] }),
105
+ { wrapper }
106
+ )
107
+
108
+ await act(async () => {
109
+ result.current.mutate({})
110
+ })
111
+
112
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
113
+
114
+ expect(result.current.data).toEqual(responseData)
115
+ })
116
+ })