@umituz/react-native-ai-generation-content 1.61.39 → 1.61.41
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/infrastructure/repositories/CreationsWriter.ts +16 -213
- package/src/domains/creations/infrastructure/repositories/creations-operations.ts +128 -0
- package/src/domains/creations/infrastructure/repositories/creations-state-operations.ts +86 -0
- package/src/domains/creations/presentation/components/GalleryScreenHeader.tsx +54 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +23 -56
- package/src/domains/creations/presentation/utils/filter-buttons.util.ts +75 -0
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +36 -43
- package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +6 -39
- package/src/domains/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +7 -25
- package/src/features/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +26 -46
- package/src/features/shared/index.ts +6 -0
- package/src/features/shared/presentation/components/AutoSkipPreview.tsx +24 -0
- package/src/features/shared/presentation/components/index.ts +6 -0
- package/src/features/shared/presentation/utils/index.ts +14 -0
- package/src/features/shared/presentation/utils/wizard-flow.utils.ts +84 -0
- package/src/features/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +26 -46
- package/src/features/text-to-video/presentation/screens/TextToVideoWizardFlow.tsx +26 -46
- package/src/infrastructure/logging/debug.util.ts +1 -1
- package/src/infrastructure/logging/index.ts +1 -1
- package/src/infrastructure/validation/advanced-validator.ts +97 -0
- package/src/infrastructure/validation/ai-validator.ts +77 -0
- package/src/infrastructure/validation/base-validator.ts +149 -0
- package/src/infrastructure/validation/entity-validator.ts +64 -0
- package/src/infrastructure/validation/input-validator.ts +37 -409
- package/src/infrastructure/validation/sanitizer.ts +43 -0
- package/src/presentation/components/buttons/ContinueButton.tsx +72 -0
- package/src/presentation/components/buttons/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.41",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
declare const __DEV__: boolean;
|
|
1
|
+
/**
|
|
2
|
+
* Creations Writer
|
|
3
|
+
* Main class that orchestrates all creation write operations
|
|
4
|
+
*/
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
] as const;
|
|
6
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
+
import * as operations from "./creations-operations";
|
|
9
|
+
import * as stateOperations from "./creations-state-operations";
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
12
|
* Handles write operations for creations
|
|
@@ -18,228 +15,34 @@ export class CreationsWriter {
|
|
|
18
15
|
constructor(private readonly pathResolver: FirestorePathResolver) {}
|
|
19
16
|
|
|
20
17
|
async create(userId: string, creation: Creation): Promise<void> {
|
|
21
|
-
|
|
22
|
-
console.log("[CreationsWriter] create()", { userId, creationId: creation.id });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const docRef = this.pathResolver.getDocRef(userId, creation.id);
|
|
26
|
-
if (!docRef) throw new Error("Firestore not initialized");
|
|
27
|
-
|
|
28
|
-
const data: CreationDocument = {
|
|
29
|
-
type: creation.type,
|
|
30
|
-
uri: creation.uri,
|
|
31
|
-
createdAt: creation.createdAt,
|
|
32
|
-
metadata: creation.metadata || {},
|
|
33
|
-
isShared: creation.isShared || false,
|
|
34
|
-
isFavorite: creation.isFavorite || false,
|
|
35
|
-
...(creation.status !== undefined && { status: creation.status }),
|
|
36
|
-
...(creation.output !== undefined && { output: creation.output }),
|
|
37
|
-
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
38
|
-
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
39
|
-
...(creation.model !== undefined && { model: creation.model }),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await setDoc(docRef, data);
|
|
44
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[CreationsWriter] create() success");
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
-
console.error("[CreationsWriter] create() error", {
|
|
49
|
-
userId,
|
|
50
|
-
creationId: creation.id,
|
|
51
|
-
error: errorMessage,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
|
|
55
|
-
}
|
|
18
|
+
return operations.createCreation(this.pathResolver, userId, creation);
|
|
56
19
|
}
|
|
57
20
|
|
|
58
21
|
async update(userId: string, id: string, updates: Partial<Creation>): Promise<boolean> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const docRef = this.pathResolver.getDocRef(userId, id);
|
|
62
|
-
if (!docRef) {
|
|
63
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
-
console.error("[CreationsWriter] update() - Firestore not initialized", { userId, id });
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const updateData: Record<string, unknown> = {};
|
|
71
|
-
for (const field of UPDATABLE_FIELDS) {
|
|
72
|
-
if (updates[field] !== undefined) updateData[field] = updates[field];
|
|
73
|
-
}
|
|
74
|
-
await updateDoc(docRef, updateData);
|
|
75
|
-
return true;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
78
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
|
-
console.error("[CreationsWriter] update() error", {
|
|
80
|
-
userId,
|
|
81
|
-
creationId: id,
|
|
82
|
-
error: errorMessage,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
22
|
+
return operations.updateCreation(this.pathResolver, userId, id, updates);
|
|
87
23
|
}
|
|
88
24
|
|
|
89
25
|
async delete(userId: string, creationId: string): Promise<boolean> {
|
|
90
|
-
|
|
91
|
-
if (!docRef) return false;
|
|
92
|
-
try {
|
|
93
|
-
await updateDoc(docRef, { deletedAt: new Date() });
|
|
94
|
-
return true;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
97
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
-
console.error("[CreationsWriter] delete() error", {
|
|
99
|
-
userId,
|
|
100
|
-
creationId,
|
|
101
|
-
error: errorMessage,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
26
|
+
return operations.deleteCreation(this.pathResolver, userId, creationId);
|
|
106
27
|
}
|
|
107
28
|
|
|
108
29
|
async hardDelete(userId: string, creationId: string): Promise<boolean> {
|
|
109
|
-
|
|
110
|
-
if (!docRef) return false;
|
|
111
|
-
try {
|
|
112
|
-
await deleteDoc(docRef);
|
|
113
|
-
return true;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
117
|
-
console.error("[CreationsWriter] hardDelete() error", {
|
|
118
|
-
userId,
|
|
119
|
-
creationId,
|
|
120
|
-
error: errorMessage,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
30
|
+
return operations.hardDeleteCreation(this.pathResolver, userId, creationId);
|
|
125
31
|
}
|
|
126
32
|
|
|
127
33
|
async restore(userId: string, creationId: string): Promise<boolean> {
|
|
128
|
-
|
|
129
|
-
if (!docRef) return false;
|
|
130
|
-
try {
|
|
131
|
-
await updateDoc(docRef, { deletedAt: null });
|
|
132
|
-
return true;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
135
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
136
|
-
console.error("[CreationsWriter] restore() error", {
|
|
137
|
-
userId,
|
|
138
|
-
creationId,
|
|
139
|
-
error: errorMessage,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
34
|
+
return operations.restoreCreation(this.pathResolver, userId, creationId);
|
|
144
35
|
}
|
|
145
36
|
|
|
146
37
|
async updateShared(userId: string, creationId: string, isShared: boolean): Promise<boolean> {
|
|
147
|
-
|
|
148
|
-
if (!docRef) return false;
|
|
149
|
-
try {
|
|
150
|
-
await updateDoc(docRef, { isShared });
|
|
151
|
-
return true;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
-
console.error("[CreationsWriter] updateShared() error", {
|
|
156
|
-
userId,
|
|
157
|
-
creationId,
|
|
158
|
-
isShared,
|
|
159
|
-
error: errorMessage,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
38
|
+
return stateOperations.updateCreationShared(this.pathResolver, userId, creationId, isShared);
|
|
164
39
|
}
|
|
165
40
|
|
|
166
41
|
async updateFavorite(userId: string, creationId: string, isFavorite: boolean): Promise<boolean> {
|
|
167
|
-
|
|
168
|
-
console.log("[CreationsWriter] updateFavorite()", { userId, creationId, isFavorite });
|
|
169
|
-
}
|
|
170
|
-
const docRef = this.pathResolver.getDocRef(userId, creationId);
|
|
171
|
-
if (!docRef) {
|
|
172
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
173
|
-
console.warn("[CreationsWriter] updateFavorite() - Firestore not initialized", {
|
|
174
|
-
userId,
|
|
175
|
-
creationId,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
await updateDoc(docRef, { isFavorite });
|
|
182
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
183
|
-
console.log("[CreationsWriter] updateFavorite() success");
|
|
184
|
-
}
|
|
185
|
-
return true;
|
|
186
|
-
} catch (error) {
|
|
187
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
188
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
189
|
-
console.error("[CreationsWriter] updateFavorite() error", {
|
|
190
|
-
userId,
|
|
191
|
-
creationId,
|
|
192
|
-
isFavorite,
|
|
193
|
-
error: errorMessage,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
42
|
+
return stateOperations.updateCreationFavorite(this.pathResolver, userId, creationId, isFavorite);
|
|
198
43
|
}
|
|
199
44
|
|
|
200
45
|
async rate(userId: string, creationId: string, rating: number, description?: string): Promise<boolean> {
|
|
201
|
-
|
|
202
|
-
if (!docRef) return false;
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
await updateDoc(docRef, { rating, ratedAt: new Date() });
|
|
206
|
-
if (description || rating) {
|
|
207
|
-
try {
|
|
208
|
-
await submitFeedback({
|
|
209
|
-
userId,
|
|
210
|
-
userEmail: null,
|
|
211
|
-
type: "creation_rating",
|
|
212
|
-
title: `Creation Rating: ${rating} Stars`,
|
|
213
|
-
description: description || `User rated creation ${rating} stars`,
|
|
214
|
-
rating,
|
|
215
|
-
status: "pending",
|
|
216
|
-
});
|
|
217
|
-
} catch (feedbackError) {
|
|
218
|
-
// Log but don't fail - the rating was saved successfully
|
|
219
|
-
const feedbackErrorMessage = feedbackError instanceof Error
|
|
220
|
-
? feedbackError.message
|
|
221
|
-
: String(feedbackError);
|
|
222
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
223
|
-
console.warn("[CreationsWriter] rate() - feedback submission failed", {
|
|
224
|
-
userId,
|
|
225
|
-
creationId,
|
|
226
|
-
error: feedbackErrorMessage,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return true;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
234
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
235
|
-
console.error("[CreationsWriter] rate() error", {
|
|
236
|
-
userId,
|
|
237
|
-
creationId,
|
|
238
|
-
rating,
|
|
239
|
-
error: errorMessage,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
46
|
+
return stateOperations.rateCreation(this.pathResolver, userId, creationId, rating, description);
|
|
244
47
|
}
|
|
245
48
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation CRUD Operations
|
|
3
|
+
* Core create, read, update, delete operations for creations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { setDoc, updateDoc, deleteDoc } from "firebase/firestore";
|
|
7
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
8
|
+
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
9
|
+
|
|
10
|
+
const UPDATABLE_FIELDS = [
|
|
11
|
+
"metadata", "isShared", "uri", "type", "prompt", "status",
|
|
12
|
+
"output", "rating", "ratedAt", "isFavorite", "deletedAt",
|
|
13
|
+
"requestId", "model",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new creation document
|
|
18
|
+
*/
|
|
19
|
+
export async function createCreation(
|
|
20
|
+
pathResolver: FirestorePathResolver,
|
|
21
|
+
userId: string,
|
|
22
|
+
creation: Creation
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const docRef = pathResolver.getDocRef(userId, creation.id);
|
|
25
|
+
if (!docRef) throw new Error("Firestore not initialized");
|
|
26
|
+
|
|
27
|
+
const data: CreationDocument = {
|
|
28
|
+
type: creation.type,
|
|
29
|
+
uri: creation.uri,
|
|
30
|
+
createdAt: creation.createdAt,
|
|
31
|
+
metadata: creation.metadata || {},
|
|
32
|
+
isShared: creation.isShared || false,
|
|
33
|
+
isFavorite: creation.isFavorite || false,
|
|
34
|
+
...(creation.status !== undefined && { status: creation.status }),
|
|
35
|
+
...(creation.output !== undefined && { output: creation.output }),
|
|
36
|
+
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
37
|
+
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
38
|
+
...(creation.model !== undefined && { model: creation.model }),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await setDoc(docRef, data);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Updates a creation document
|
|
51
|
+
*/
|
|
52
|
+
export async function updateCreation(
|
|
53
|
+
pathResolver: FirestorePathResolver,
|
|
54
|
+
userId: string,
|
|
55
|
+
id: string,
|
|
56
|
+
updates: Partial<Creation>
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
const docRef = pathResolver.getDocRef(userId, id);
|
|
59
|
+
if (!docRef) return false;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const updateData: Record<string, unknown> = {};
|
|
63
|
+
for (const field of UPDATABLE_FIELDS) {
|
|
64
|
+
if (updates[field] !== undefined) updateData[field] = updates[field];
|
|
65
|
+
}
|
|
66
|
+
await updateDoc(docRef, updateData);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Soft deletes a creation (marks as deleted)
|
|
75
|
+
*/
|
|
76
|
+
export async function deleteCreation(
|
|
77
|
+
pathResolver: FirestorePathResolver,
|
|
78
|
+
userId: string,
|
|
79
|
+
creationId: string
|
|
80
|
+
): Promise<boolean> {
|
|
81
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
82
|
+
if (!docRef) return false;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await updateDoc(docRef, { deletedAt: new Date() });
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hard deletes a creation (removes from database)
|
|
94
|
+
*/
|
|
95
|
+
export async function hardDeleteCreation(
|
|
96
|
+
pathResolver: FirestorePathResolver,
|
|
97
|
+
userId: string,
|
|
98
|
+
creationId: string
|
|
99
|
+
): Promise<boolean> {
|
|
100
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
101
|
+
if (!docRef) return false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await deleteDoc(docRef);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Restores a soft-deleted creation
|
|
113
|
+
*/
|
|
114
|
+
export async function restoreCreation(
|
|
115
|
+
pathResolver: FirestorePathResolver,
|
|
116
|
+
userId: string,
|
|
117
|
+
creationId: string
|
|
118
|
+
): Promise<boolean> {
|
|
119
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
120
|
+
if (!docRef) return false;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await updateDoc(docRef, { deletedAt: null });
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation State Operations
|
|
3
|
+
* State-specific operations like sharing, favoriting, and rating
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { updateDoc } from "firebase/firestore";
|
|
7
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
8
|
+
import { submitFeedback } from "@umituz/react-native-subscription";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Updates the shared status of a creation
|
|
12
|
+
*/
|
|
13
|
+
export async function updateCreationShared(
|
|
14
|
+
pathResolver: FirestorePathResolver,
|
|
15
|
+
userId: string,
|
|
16
|
+
creationId: string,
|
|
17
|
+
isShared: boolean
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
20
|
+
if (!docRef) return false;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await updateDoc(docRef, { isShared });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Updates the favorite status of a creation
|
|
32
|
+
*/
|
|
33
|
+
export async function updateCreationFavorite(
|
|
34
|
+
pathResolver: FirestorePathResolver,
|
|
35
|
+
userId: string,
|
|
36
|
+
creationId: string,
|
|
37
|
+
isFavorite: boolean
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
40
|
+
if (!docRef) return false;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await updateDoc(docRef, { isFavorite });
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rates a creation and optionally submits feedback
|
|
52
|
+
*/
|
|
53
|
+
export async function rateCreation(
|
|
54
|
+
pathResolver: FirestorePathResolver,
|
|
55
|
+
userId: string,
|
|
56
|
+
creationId: string,
|
|
57
|
+
rating: number,
|
|
58
|
+
description?: string
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
61
|
+
if (!docRef) return false;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await updateDoc(docRef, { rating, ratedAt: new Date() });
|
|
65
|
+
|
|
66
|
+
// Submit feedback if description or rating is provided
|
|
67
|
+
if (description || rating) {
|
|
68
|
+
try {
|
|
69
|
+
await submitFeedback({
|
|
70
|
+
userId,
|
|
71
|
+
userEmail: null,
|
|
72
|
+
type: "creation_rating",
|
|
73
|
+
title: `Creation Rating: ${rating} Stars`,
|
|
74
|
+
description: description || `User rated creation ${rating} stars`,
|
|
75
|
+
rating,
|
|
76
|
+
status: "pending",
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
// Feedback submission failed but rating was saved - continue
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GalleryScreenHeader Component
|
|
3
|
+
* Header component for the gallery screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens, AtomicIcon, AtomicText, type DesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
export interface GalleryScreenHeaderProps {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly onBack: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GalleryScreenHeader: React.FC<GalleryScreenHeaderProps> = ({ title, onBack }) => {
|
|
16
|
+
const tokens = useAppDesignTokens();
|
|
17
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.screenHeader}>
|
|
21
|
+
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
22
|
+
<AtomicIcon
|
|
23
|
+
name="chevron-left"
|
|
24
|
+
customSize={28}
|
|
25
|
+
customColor={tokens.colors.textPrimary}
|
|
26
|
+
/>
|
|
27
|
+
</TouchableOpacity>
|
|
28
|
+
<AtomicText
|
|
29
|
+
type="titleLarge"
|
|
30
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
31
|
+
>
|
|
32
|
+
{title}
|
|
33
|
+
</AtomicText>
|
|
34
|
+
<View style={styles.placeholder} />
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
40
|
+
StyleSheet.create({
|
|
41
|
+
screenHeader: {
|
|
42
|
+
flexDirection: "row",
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
paddingHorizontal: tokens.spacing.md,
|
|
45
|
+
paddingVertical: tokens.spacing.sm,
|
|
46
|
+
gap: tokens.spacing.md,
|
|
47
|
+
},
|
|
48
|
+
backButton: {
|
|
49
|
+
padding: tokens.spacing.xs,
|
|
50
|
+
},
|
|
51
|
+
placeholder: {
|
|
52
|
+
width: 36,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import { View, FlatList, RefreshControl
|
|
2
|
+
import { View, FlatList, RefreshControl } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
useAppDesignTokens,
|
|
5
5
|
FilterSheet,
|
|
6
6
|
ScreenLayout,
|
|
7
7
|
useAppFocusEffect,
|
|
8
|
-
AtomicIcon,
|
|
9
|
-
AtomicText,
|
|
10
8
|
} from "@umituz/react-native-design-system";
|
|
11
9
|
import { useCreations } from "../hooks/useCreations";
|
|
12
10
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
@@ -15,8 +13,10 @@ import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
|
15
13
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
16
14
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
17
15
|
import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
16
|
+
import { GalleryScreenHeader } from "../components/GalleryScreenHeader";
|
|
18
17
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
19
18
|
import { getPreviewUrl } from "../../domain/utils";
|
|
19
|
+
import { createFilterButtons, createItemTitle } from "../utils/filter-buttons.util";
|
|
20
20
|
import type { Creation } from "../../domain/entities/Creation";
|
|
21
21
|
import type { CreationsGalleryScreenProps } from "./creations-gallery.types";
|
|
22
22
|
import { creationsGalleryStyles as styles } from "./creations-gallery.styles";
|
|
@@ -44,7 +44,6 @@ export function CreationsGalleryScreen({
|
|
|
44
44
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
45
45
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
46
46
|
|
|
47
|
-
// Poll FAL queue for "processing" creations (enables true background generation)
|
|
48
47
|
useProcessingJobsPoller({
|
|
49
48
|
userId,
|
|
50
49
|
creations: creations ?? [],
|
|
@@ -84,38 +83,24 @@ export function CreationsGalleryScreen({
|
|
|
84
83
|
|
|
85
84
|
useAppFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
|
|
86
85
|
|
|
87
|
-
const filterButtons = useMemo(() =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
id: "media",
|
|
101
|
-
label: t(config.translations.mediaFilterTitle ?? "creations.filter.media"),
|
|
102
|
-
icon: "grid-outline",
|
|
103
|
-
isActive: filters.mediaFilter.hasActiveFilter,
|
|
104
|
-
onPress: filters.openMediaFilter,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return buttons;
|
|
108
|
-
}, [showStatusFilter, showMediaFilter, filters, t, config.translations]);
|
|
86
|
+
const filterButtons = useMemo(() =>
|
|
87
|
+
createFilterButtons({
|
|
88
|
+
showStatusFilter,
|
|
89
|
+
showMediaFilter,
|
|
90
|
+
statusFilterActive: filters.statusFilter.hasActiveFilter,
|
|
91
|
+
mediaFilterActive: filters.mediaFilter.hasActiveFilter,
|
|
92
|
+
statusFilterLabel: t(config.translations.statusFilterTitle ?? "creations.filter.status"),
|
|
93
|
+
mediaFilterLabel: t(config.translations.mediaFilterTitle ?? "creations.filter.media"),
|
|
94
|
+
onStatusFilterPress: filters.openStatusFilter,
|
|
95
|
+
onMediaFilterPress: filters.openMediaFilter,
|
|
96
|
+
}),
|
|
97
|
+
[showStatusFilter, showMediaFilter, filters, t, config.translations]
|
|
98
|
+
);
|
|
109
99
|
|
|
110
|
-
const getItemTitle = useCallback((item: Creation): string =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
// Default: use type config label
|
|
116
|
-
const typeConfig = config.types?.find((tc) => tc.id === item.type);
|
|
117
|
-
return typeConfig?.labelKey ? t(typeConfig.labelKey) : item.type.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
118
|
-
}, [config.types, t, getCreationTitle]);
|
|
100
|
+
const getItemTitle = useCallback((item: Creation): string =>
|
|
101
|
+
createItemTitle(item, { types: config.types, getCreationTitle }, t),
|
|
102
|
+
[config.types, t, getCreationTitle]
|
|
103
|
+
);
|
|
119
104
|
|
|
120
105
|
const renderItem = useCallback(({ item }: { item: Creation }) => (
|
|
121
106
|
<CreationCard
|
|
@@ -143,7 +128,7 @@ export function CreationsGalleryScreen({
|
|
|
143
128
|
/>
|
|
144
129
|
</View>
|
|
145
130
|
);
|
|
146
|
-
}, [creations, isLoading, filters.filtered.length,
|
|
131
|
+
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens]);
|
|
147
132
|
|
|
148
133
|
const renderEmpty = useMemo(() => (
|
|
149
134
|
<GalleryEmptyStates
|
|
@@ -167,26 +152,8 @@ export function CreationsGalleryScreen({
|
|
|
167
152
|
|
|
168
153
|
const screenHeader = useMemo(() => {
|
|
169
154
|
if (!onBack) return undefined;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<View style={styles.screenHeader}>
|
|
173
|
-
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
174
|
-
<AtomicIcon
|
|
175
|
-
name="chevron-left"
|
|
176
|
-
customSize={28}
|
|
177
|
-
customColor={tokens.colors.textPrimary}
|
|
178
|
-
/>
|
|
179
|
-
</TouchableOpacity>
|
|
180
|
-
<AtomicText
|
|
181
|
-
type="titleLarge"
|
|
182
|
-
style={{ color: tokens.colors.textPrimary }}
|
|
183
|
-
>
|
|
184
|
-
{t(config.translations.title)}
|
|
185
|
-
</AtomicText>
|
|
186
|
-
<View style={styles.placeholder} />
|
|
187
|
-
</View>
|
|
188
|
-
);
|
|
189
|
-
}, [onBack, tokens, t, config]);
|
|
155
|
+
return <GalleryScreenHeader title={t(config.translations.title)} onBack={onBack} />;
|
|
156
|
+
}, [onBack, t, config.translations.title]);
|
|
190
157
|
|
|
191
158
|
if (showPreview) {
|
|
192
159
|
return (
|