@umituz/react-native-ai-generation-content 1.72.10 → 1.72.12
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/package.json +1 -1
- package/src/domains/creations/presentation/components/CreationCard.utils.ts +2 -3
- package/src/domains/creations/presentation/components/CreationsFilterBar.tsx +41 -5
- package/src/domains/creations/presentation/hooks/filterHelpers.ts +5 -17
- package/src/domains/image-to-video/presentation/hooks/useFormState.ts +30 -58
- package/src/domains/image-to-video/presentation/hooks/useGeneration.ts +41 -71
- package/src/domains/text-to-image/presentation/hooks/useFormState.ts +34 -81
- package/src/exports/presentation.ts +5 -5
- package/src/index.ts +3 -0
- package/src/presentation/components/GenerationProgressContent.tsx +5 -2
- package/src/presentation/components/PendingJobCard.tsx +9 -2
- package/src/presentation/components/README.md +6 -5
- package/src/presentation/components/index.ts +0 -4
- package/src/shared/components/common/ProgressBar.tsx +99 -0
- package/src/shared/components/common/index.ts +5 -0
- package/src/shared/components/index.ts +5 -0
- package/src/shared/hooks/factories/createFormStateHook.ts +119 -0
- package/src/shared/hooks/factories/createGenerationHook.ts +253 -0
- package/src/shared/hooks/factories/index.ts +21 -0
- package/src/shared/hooks/index.ts +5 -0
- package/src/shared/index.ts +14 -0
- package/src/shared/utils/date/index.ts +11 -0
- package/src/shared/utils/date/normalization.ts +60 -0
- package/src/shared/utils/filters/createFilterButtons.ts +60 -0
- package/src/shared/utils/filters/index.ts +11 -0
- package/src/shared/utils/index.ts +6 -0
- package/src/domains/creations/presentation/components/filter-bar-utils.ts +0 -96
- package/src/presentation/components/GenerationProgressBar.tsx +0 -78
- package/src/presentation/components/PendingJobProgressBar.tsx +0 -56
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export { GenerationProgressContent } from "./GenerationProgressContent";
|
|
2
|
-
export { GenerationProgressBar } from "./GenerationProgressBar";
|
|
3
2
|
export { PendingJobCard } from "./PendingJobCard";
|
|
4
|
-
export { PendingJobProgressBar } from "./PendingJobProgressBar";
|
|
5
3
|
export { PendingJobCardActions } from "./PendingJobCardActions";
|
|
6
4
|
export { PromptInput } from "./PromptInput";
|
|
7
5
|
export { AIGenerationHero } from "./AIGenerationHero";
|
|
@@ -12,14 +10,12 @@ export * from "./AIGenerationForm.types";
|
|
|
12
10
|
export * from "./AIGenerationConfig";
|
|
13
11
|
|
|
14
12
|
export type { GenerationProgressContentProps } from "./GenerationProgressContent";
|
|
15
|
-
export type { GenerationProgressBarProps } from "./GenerationProgressBar";
|
|
16
13
|
|
|
17
14
|
export type {
|
|
18
15
|
PendingJobCardProps,
|
|
19
16
|
StatusLabels,
|
|
20
17
|
} from "./PendingJobCard";
|
|
21
18
|
|
|
22
|
-
export type { PendingJobProgressBarProps } from "./PendingJobProgressBar";
|
|
23
19
|
export type { PendingJobCardActionsProps } from "./PendingJobCardActions";
|
|
24
20
|
export type { PromptInputProps } from "./PromptInput";
|
|
25
21
|
export type { AIGenerationHeroProps } from "./AIGenerationHero";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified ProgressBar Component
|
|
3
|
+
* Flexible progress bar with optional percentage display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import { clampProgress, roundProgress } from "../../../infrastructure/utils/progress.utils";
|
|
10
|
+
|
|
11
|
+
export interface ProgressBarProps {
|
|
12
|
+
/** Progress value (0-100) */
|
|
13
|
+
progress: number;
|
|
14
|
+
/** Show percentage text below bar */
|
|
15
|
+
showPercentage?: boolean;
|
|
16
|
+
/** Custom text color */
|
|
17
|
+
textColor?: string;
|
|
18
|
+
/** Custom progress fill color */
|
|
19
|
+
progressColor?: string;
|
|
20
|
+
/** Custom background color */
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
/** Bar height (default: 8 for standard, 4 for compact) */
|
|
23
|
+
height?: number;
|
|
24
|
+
/** Spacing below bar (default: 16 with percentage, 0 without) */
|
|
25
|
+
marginBottom?: number;
|
|
26
|
+
/** Border radius (default: height / 2) */
|
|
27
|
+
borderRadius?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ProgressBar: React.FC<ProgressBarProps> = ({
|
|
31
|
+
progress,
|
|
32
|
+
showPercentage = false,
|
|
33
|
+
textColor,
|
|
34
|
+
progressColor,
|
|
35
|
+
backgroundColor,
|
|
36
|
+
height = 8,
|
|
37
|
+
marginBottom,
|
|
38
|
+
borderRadius,
|
|
39
|
+
}) => {
|
|
40
|
+
const tokens = useAppDesignTokens();
|
|
41
|
+
const clampedProgress = clampProgress(progress);
|
|
42
|
+
const actualBorderRadius = borderRadius ?? height / 2;
|
|
43
|
+
const actualMarginBottom = marginBottom ?? (showPercentage ? 16 : 0);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={[styles.container, { marginBottom: actualMarginBottom }]}>
|
|
47
|
+
<View
|
|
48
|
+
style={[
|
|
49
|
+
styles.background,
|
|
50
|
+
{
|
|
51
|
+
backgroundColor: backgroundColor || tokens.colors.borderLight,
|
|
52
|
+
height,
|
|
53
|
+
borderRadius: actualBorderRadius,
|
|
54
|
+
},
|
|
55
|
+
]}
|
|
56
|
+
>
|
|
57
|
+
<View
|
|
58
|
+
style={[
|
|
59
|
+
styles.fill,
|
|
60
|
+
{
|
|
61
|
+
backgroundColor: progressColor || tokens.colors.primary,
|
|
62
|
+
width: `${clampedProgress}%`,
|
|
63
|
+
borderRadius: actualBorderRadius,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
</View>
|
|
68
|
+
{showPercentage && (
|
|
69
|
+
<AtomicText
|
|
70
|
+
style={[
|
|
71
|
+
styles.text,
|
|
72
|
+
{ color: textColor || tokens.colors.textPrimary },
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{roundProgress(clampedProgress)}%
|
|
76
|
+
</AtomicText>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const styles = StyleSheet.create({
|
|
83
|
+
container: {
|
|
84
|
+
width: "100%",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
},
|
|
87
|
+
background: {
|
|
88
|
+
width: "100%",
|
|
89
|
+
overflow: "hidden",
|
|
90
|
+
},
|
|
91
|
+
fill: {
|
|
92
|
+
height: "100%",
|
|
93
|
+
},
|
|
94
|
+
text: {
|
|
95
|
+
fontSize: 14,
|
|
96
|
+
fontWeight: "600",
|
|
97
|
+
marginTop: 8,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Form State Hook Factory
|
|
3
|
+
* Creates type-safe form state management hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Form state configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface FormStateConfig<TState, TDefaults> {
|
|
12
|
+
/** Initial state factory */
|
|
13
|
+
createInitialState: (defaults: TDefaults) => TState;
|
|
14
|
+
/** Optional state validator */
|
|
15
|
+
validate?: (state: TState) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Form actions type - maps state keys to setters
|
|
20
|
+
*/
|
|
21
|
+
export type FormActions<TState> = {
|
|
22
|
+
[K in keyof TState as `set${Capitalize<string & K>}`]: (value: TState[K]) => void;
|
|
23
|
+
} & {
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Form state hook return type
|
|
29
|
+
*/
|
|
30
|
+
export interface FormStateHookReturn<TState, TActions> {
|
|
31
|
+
state: TState;
|
|
32
|
+
actions: TActions;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for form state hook
|
|
37
|
+
*/
|
|
38
|
+
export interface FormStateHookOptions<TDefaults> {
|
|
39
|
+
defaults: TDefaults;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a type-safe form state hook
|
|
44
|
+
* @param config - Form state configuration
|
|
45
|
+
* @returns Form state hook
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const useMyForm = createFormStateHook({
|
|
50
|
+
* createInitialState: (defaults) => ({
|
|
51
|
+
* name: "",
|
|
52
|
+
* age: defaults.age,
|
|
53
|
+
* }),
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Usage
|
|
57
|
+
* const { state, actions } = useMyForm({ defaults: { age: 18 } });
|
|
58
|
+
* actions.setName("John");
|
|
59
|
+
* actions.setAge(25);
|
|
60
|
+
* actions.reset();
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createFormStateHook<
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
TState extends Record<string, any>,
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
TDefaults extends Record<string, any>,
|
|
68
|
+
TActions extends FormActions<TState> = FormActions<TState>
|
|
69
|
+
>(
|
|
70
|
+
config: FormStateConfig<TState, TDefaults>
|
|
71
|
+
) {
|
|
72
|
+
return function useFormState(
|
|
73
|
+
options: FormStateHookOptions<TDefaults>
|
|
74
|
+
): FormStateHookReturn<TState, TActions> {
|
|
75
|
+
const { defaults } = options;
|
|
76
|
+
|
|
77
|
+
// Validate defaults if validator provided
|
|
78
|
+
const validatedDefaults = useMemo(() => {
|
|
79
|
+
if (config.validate) {
|
|
80
|
+
const initialState = config.createInitialState(defaults);
|
|
81
|
+
config.validate(initialState);
|
|
82
|
+
}
|
|
83
|
+
return defaults;
|
|
84
|
+
}, [defaults]);
|
|
85
|
+
|
|
86
|
+
// Create initial state
|
|
87
|
+
const [state, setState] = useState<TState>(() =>
|
|
88
|
+
config.createInitialState(validatedDefaults)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Create reset function
|
|
92
|
+
const reset = useCallback(() => {
|
|
93
|
+
setState(config.createInitialState(validatedDefaults));
|
|
94
|
+
}, [validatedDefaults]);
|
|
95
|
+
|
|
96
|
+
// Create actions dynamically
|
|
97
|
+
const actions = useMemo(() => {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
const actionObj: any = { reset };
|
|
100
|
+
|
|
101
|
+
// Generate setter for each state key
|
|
102
|
+
Object.keys(state).forEach((key) => {
|
|
103
|
+
const setterName = `set${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
actionObj[setterName] = (value: any) => {
|
|
106
|
+
setState((prev) => ({ ...prev, [key]: value }));
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return actionObj as TActions;
|
|
111
|
+
}, [state, reset]);
|
|
112
|
+
|
|
113
|
+
return { state, actions };
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Note: createFormStateHookWithIndividualState removed due to type complexity
|
|
118
|
+
// and lack of usage. Use createFormStateHook instead which provides the same
|
|
119
|
+
// functionality with better type safety.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Generation Hook Factory
|
|
3
|
+
* Creates type-safe generation hooks with error handling, progress tracking, and abort support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generation state
|
|
10
|
+
*/
|
|
11
|
+
export interface GenerationState {
|
|
12
|
+
isGenerating: boolean;
|
|
13
|
+
progress: number;
|
|
14
|
+
error: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generation callbacks
|
|
19
|
+
*/
|
|
20
|
+
export interface GenerationCallbacks<TResult> {
|
|
21
|
+
/** Called when generation succeeds */
|
|
22
|
+
onSuccess?: (result: TResult) => void;
|
|
23
|
+
/** Called when generation fails */
|
|
24
|
+
onError?: (error: string) => void;
|
|
25
|
+
/** Called on progress update */
|
|
26
|
+
onProgress?: (progress: number) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generation hook configuration
|
|
31
|
+
*/
|
|
32
|
+
export interface GenerationHookConfig<TRequest, TResult> {
|
|
33
|
+
/** Execute the generation request */
|
|
34
|
+
execute: (request: TRequest, signal?: AbortSignal) => Promise<TResult>;
|
|
35
|
+
/** Optional validation before execution */
|
|
36
|
+
validate?: (request: TRequest) => string | null;
|
|
37
|
+
/** Optional transform for errors */
|
|
38
|
+
transformError?: (error: unknown) => string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generation hook return type
|
|
43
|
+
*/
|
|
44
|
+
export interface GenerationHookReturn<TRequest, TResult> {
|
|
45
|
+
generationState: GenerationState;
|
|
46
|
+
handleGenerate: (request: TRequest) => Promise<TResult | null>;
|
|
47
|
+
setProgress: (progress: number) => void;
|
|
48
|
+
setError: (error: string | null) => void;
|
|
49
|
+
abort: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const INITIAL_STATE: GenerationState = {
|
|
53
|
+
isGenerating: false,
|
|
54
|
+
progress: 0,
|
|
55
|
+
error: null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a type-safe generation hook
|
|
60
|
+
* @param config - Generation hook configuration
|
|
61
|
+
* @param callbacks - Generation callbacks
|
|
62
|
+
* @returns Generation hook
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const useMyGeneration = createGenerationHook({
|
|
67
|
+
* execute: async (request) => {
|
|
68
|
+
* return await api.generate(request);
|
|
69
|
+
* },
|
|
70
|
+
* validate: (request) => {
|
|
71
|
+
* if (!request.prompt) return "Prompt is required";
|
|
72
|
+
* return null;
|
|
73
|
+
* },
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Usage
|
|
77
|
+
* const { generationState, handleGenerate } = useMyGeneration({
|
|
78
|
+
* onSuccess: (result) => console.log("Success!", result),
|
|
79
|
+
* onError: (error) => console.error("Error:", error),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function createGenerationHook<TRequest, TResult>(
|
|
84
|
+
config: GenerationHookConfig<TRequest, TResult>
|
|
85
|
+
) {
|
|
86
|
+
return function useGeneration(
|
|
87
|
+
callbacks: GenerationCallbacks<TResult> = {}
|
|
88
|
+
): GenerationHookReturn<TRequest, TResult> {
|
|
89
|
+
const [generationState, setGenerationState] = useState<GenerationState>(INITIAL_STATE);
|
|
90
|
+
|
|
91
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
92
|
+
const isMountedRef = useRef(true);
|
|
93
|
+
|
|
94
|
+
// Stabilize callbacks
|
|
95
|
+
const onSuccessRef = useRef(callbacks.onSuccess);
|
|
96
|
+
const onErrorRef = useRef(callbacks.onError);
|
|
97
|
+
const onProgressRef = useRef(callbacks.onProgress);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
onSuccessRef.current = callbacks.onSuccess;
|
|
101
|
+
onErrorRef.current = callbacks.onError;
|
|
102
|
+
onProgressRef.current = callbacks.onProgress;
|
|
103
|
+
}, [callbacks.onSuccess, callbacks.onError, callbacks.onProgress]);
|
|
104
|
+
|
|
105
|
+
// Cleanup on unmount
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
isMountedRef.current = true;
|
|
108
|
+
return () => {
|
|
109
|
+
isMountedRef.current = false;
|
|
110
|
+
abortControllerRef.current?.abort();
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const setProgress = useCallback((progress: number) => {
|
|
115
|
+
if (!isMountedRef.current) return;
|
|
116
|
+
setGenerationState((prev) => ({ ...prev, progress }));
|
|
117
|
+
onProgressRef.current?.(progress);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const setError = useCallback((error: string | null) => {
|
|
121
|
+
if (!isMountedRef.current) return;
|
|
122
|
+
setGenerationState((prev) => ({ ...prev, error, isGenerating: false }));
|
|
123
|
+
if (error) {
|
|
124
|
+
onErrorRef.current?.(error);
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const abort = useCallback(() => {
|
|
129
|
+
abortControllerRef.current?.abort();
|
|
130
|
+
if (isMountedRef.current) {
|
|
131
|
+
setGenerationState((prev) => ({
|
|
132
|
+
...prev,
|
|
133
|
+
isGenerating: false,
|
|
134
|
+
error: "Generation aborted",
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const handleGenerate = useCallback(
|
|
140
|
+
async (request: TRequest): Promise<TResult | null> => {
|
|
141
|
+
// Validate request
|
|
142
|
+
if (config.validate) {
|
|
143
|
+
const validationError = config.validate(request);
|
|
144
|
+
if (validationError) {
|
|
145
|
+
setError(validationError);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create new AbortController
|
|
151
|
+
abortControllerRef.current = new AbortController();
|
|
152
|
+
|
|
153
|
+
setGenerationState({
|
|
154
|
+
isGenerating: true,
|
|
155
|
+
progress: 0,
|
|
156
|
+
error: null,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const result = await config.execute(
|
|
161
|
+
request,
|
|
162
|
+
abortControllerRef.current.signal
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!isMountedRef.current || abortControllerRef.current.signal.aborted) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setGenerationState((prev) => ({
|
|
170
|
+
...prev,
|
|
171
|
+
isGenerating: false,
|
|
172
|
+
progress: 100
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
onSuccessRef.current?.(result);
|
|
176
|
+
return result;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!isMountedRef.current || abortControllerRef.current.signal.aborted) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const errorMessage = config.transformError
|
|
183
|
+
? config.transformError(error)
|
|
184
|
+
: error instanceof Error
|
|
185
|
+
? error.message
|
|
186
|
+
: String(error);
|
|
187
|
+
|
|
188
|
+
setError(errorMessage);
|
|
189
|
+
return null;
|
|
190
|
+
} finally {
|
|
191
|
+
abortControllerRef.current = null;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[config, setError]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
generationState,
|
|
199
|
+
handleGenerate,
|
|
200
|
+
setProgress,
|
|
201
|
+
setError,
|
|
202
|
+
abort,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Creates a generation hook with progress updates
|
|
209
|
+
* Useful when the generation API supports progress callbacks
|
|
210
|
+
*/
|
|
211
|
+
export function createGenerationHookWithProgress<TRequest, TResult>(
|
|
212
|
+
config: GenerationHookConfig<TRequest, TResult> & {
|
|
213
|
+
/** Get progress stream or polling function */
|
|
214
|
+
subscribeToProgress?: (
|
|
215
|
+
request: TRequest,
|
|
216
|
+
onProgress: (progress: number) => void
|
|
217
|
+
) => () => void;
|
|
218
|
+
}
|
|
219
|
+
) {
|
|
220
|
+
const baseHook = createGenerationHook(config);
|
|
221
|
+
|
|
222
|
+
return function useGenerationWithProgress(
|
|
223
|
+
callbacks: GenerationCallbacks<TResult> = {}
|
|
224
|
+
): GenerationHookReturn<TRequest, TResult> {
|
|
225
|
+
const hookResult = baseHook(callbacks);
|
|
226
|
+
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
227
|
+
|
|
228
|
+
const handleGenerateWithProgress = useCallback(
|
|
229
|
+
async (request: TRequest): Promise<TResult | null> => {
|
|
230
|
+
// Subscribe to progress if available
|
|
231
|
+
if (config.subscribeToProgress) {
|
|
232
|
+
unsubscribeRef.current = config.subscribeToProgress(
|
|
233
|
+
request,
|
|
234
|
+
hookResult.setProgress
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
return await hookResult.handleGenerate(request);
|
|
240
|
+
} finally {
|
|
241
|
+
unsubscribeRef.current?.();
|
|
242
|
+
unsubscribeRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
[hookResult, config.subscribeToProgress]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...hookResult,
|
|
250
|
+
handleGenerate: handleGenerateWithProgress,
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Factories
|
|
3
|
+
* Generic hook creation utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
createFormStateHook,
|
|
8
|
+
type FormStateConfig,
|
|
9
|
+
type FormActions,
|
|
10
|
+
type FormStateHookReturn,
|
|
11
|
+
type FormStateHookOptions,
|
|
12
|
+
} from "./createFormStateHook";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
createGenerationHook,
|
|
16
|
+
createGenerationHookWithProgress,
|
|
17
|
+
type GenerationState as BaseGenerationState,
|
|
18
|
+
type GenerationCallbacks,
|
|
19
|
+
type GenerationHookConfig,
|
|
20
|
+
type GenerationHookReturn,
|
|
21
|
+
} from "./createGenerationHook";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Utilities, Components, and Hooks
|
|
3
|
+
* Centralized exports for all shared code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
export * from "./components";
|
|
8
|
+
|
|
9
|
+
// Hooks
|
|
10
|
+
export * from "./hooks";
|
|
11
|
+
|
|
12
|
+
// Utils
|
|
13
|
+
export * from "./utils/date";
|
|
14
|
+
export * from "./utils/filters";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Normalization Utilities
|
|
3
|
+
* Provides consistent date handling across the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a Date or timestamp to a timestamp number
|
|
8
|
+
* @param date - Date object or timestamp number
|
|
9
|
+
* @returns Timestamp in milliseconds
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeDateToTimestamp(date: Date | number | undefined): number {
|
|
12
|
+
if (date === undefined) return 0;
|
|
13
|
+
return date instanceof Date ? date.getTime() : date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes a Date or timestamp to a Date object
|
|
18
|
+
* @param date - Date object or timestamp number
|
|
19
|
+
* @returns Date object
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeToDate(date: Date | number): Date {
|
|
22
|
+
return date instanceof Date ? date : new Date(date);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a value is a valid date
|
|
27
|
+
* @param value - Value to check
|
|
28
|
+
* @returns True if value is a valid Date or number timestamp
|
|
29
|
+
*/
|
|
30
|
+
export function isValidDate(value: unknown): value is Date | number {
|
|
31
|
+
if (value instanceof Date) {
|
|
32
|
+
return !isNaN(value.getTime());
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
return !isNaN(value) && isFinite(value);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compares two dates for sorting
|
|
42
|
+
* @param a - First date
|
|
43
|
+
* @param b - Second date
|
|
44
|
+
* @param order - Sort order ('asc' or 'desc')
|
|
45
|
+
* @returns Comparison result for Array.sort()
|
|
46
|
+
*/
|
|
47
|
+
export function compareDates(
|
|
48
|
+
a: Date | number | undefined,
|
|
49
|
+
b: Date | number | undefined,
|
|
50
|
+
order: "asc" | "desc" = "asc"
|
|
51
|
+
): number {
|
|
52
|
+
const aTime = normalizeDateToTimestamp(a);
|
|
53
|
+
const bTime = normalizeDateToTimestamp(b);
|
|
54
|
+
|
|
55
|
+
if (aTime === 0 && bTime === 0) return 0;
|
|
56
|
+
if (aTime === 0) return 1;
|
|
57
|
+
if (bTime === 0) return -1;
|
|
58
|
+
|
|
59
|
+
return order === "desc" ? bTime - aTime : aTime - bTime;
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Filter Button Factory
|
|
3
|
+
* Creates filter button configurations from data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FilterButtonConfig<T = string> {
|
|
7
|
+
id: T;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: string;
|
|
10
|
+
active: boolean;
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FilterItemInput<T = string> {
|
|
15
|
+
id: T;
|
|
16
|
+
label: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates filter buttons from configuration
|
|
22
|
+
* @param items - Array of filter items with id, label, and icon
|
|
23
|
+
* @param activeFilter - Currently active filter id
|
|
24
|
+
* @param onSelect - Callback when a filter is selected
|
|
25
|
+
* @returns Array of filter button configurations
|
|
26
|
+
*/
|
|
27
|
+
export function createFilterButtons<T extends string = string>(
|
|
28
|
+
items: readonly FilterItemInput<T>[],
|
|
29
|
+
activeFilter: T,
|
|
30
|
+
onSelect: (filter: T) => void
|
|
31
|
+
): FilterButtonConfig<T>[] {
|
|
32
|
+
return items.map((item) => ({
|
|
33
|
+
id: item.id,
|
|
34
|
+
label: item.label,
|
|
35
|
+
icon: item.icon,
|
|
36
|
+
active: activeFilter === item.id,
|
|
37
|
+
onPress: () => onSelect(item.id),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates filter buttons from a record/map
|
|
43
|
+
* @param filterMap - Record of filter id to label and icon
|
|
44
|
+
* @param activeFilter - Currently active filter id
|
|
45
|
+
* @param onSelect - Callback when a filter is selected
|
|
46
|
+
* @returns Array of filter button configurations
|
|
47
|
+
*/
|
|
48
|
+
export function createFilterButtonsFromMap<T extends string = string>(
|
|
49
|
+
filterMap: Record<T, { label: string; icon: string }>,
|
|
50
|
+
activeFilter: T,
|
|
51
|
+
onSelect: (filter: T) => void
|
|
52
|
+
): FilterButtonConfig<T>[] {
|
|
53
|
+
return Object.entries(filterMap).map(([id, config]) => ({
|
|
54
|
+
id: id as T,
|
|
55
|
+
label: (config as { label: string; icon: string }).label,
|
|
56
|
+
icon: (config as { label: string; icon: string }).icon,
|
|
57
|
+
active: activeFilter === id,
|
|
58
|
+
onPress: () => onSelect(id as T),
|
|
59
|
+
}));
|
|
60
|
+
}
|