@umituz/react-native-ai-generation-content 1.48.0 → 1.48.2
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/domain/repositories/ICreationsRepository.ts +9 -0
- package/src/domains/creations/domain/repositories/index.ts +5 -1
- package/src/domains/creations/index.ts +5 -1
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +59 -2
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +13 -1
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +19 -48
- package/src/domains/creations/presentation/hooks/useCreationRating.ts +43 -29
- package/src/domains/creations/presentation/hooks/useCreations.ts +63 -18
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +42 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.2",
|
|
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",
|
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Creation } from "../entities/Creation";
|
|
7
7
|
|
|
8
|
+
export type CreationsSubscriptionCallback = (creations: Creation[]) => void;
|
|
9
|
+
export type UnsubscribeFunction = () => void;
|
|
10
|
+
|
|
8
11
|
export interface ICreationsRepository {
|
|
9
12
|
getAll(userId: string): Promise<Creation[]>;
|
|
13
|
+
/** Realtime subscription to all creations */
|
|
14
|
+
subscribeToAll(
|
|
15
|
+
userId: string,
|
|
16
|
+
onData: CreationsSubscriptionCallback,
|
|
17
|
+
onError?: (error: Error) => void,
|
|
18
|
+
): UnsubscribeFunction;
|
|
10
19
|
getById(userId: string, id: string): Promise<Creation | null>;
|
|
11
20
|
create(userId: string, creation: Creation): Promise<void>;
|
|
12
21
|
update(
|
|
@@ -94,7 +94,11 @@ export { DEFAULT_TRANSLATIONS, DEFAULT_CONFIG } from "./domain/value-objects";
|
|
|
94
94
|
// DOMAIN LAYER - Repository Interface
|
|
95
95
|
// =============================================================================
|
|
96
96
|
|
|
97
|
-
export type {
|
|
97
|
+
export type {
|
|
98
|
+
ICreationsRepository,
|
|
99
|
+
CreationsSubscriptionCallback,
|
|
100
|
+
UnsubscribeFunction,
|
|
101
|
+
} from "./domain/repositories";
|
|
98
102
|
|
|
99
103
|
// =============================================================================
|
|
100
104
|
// INFRASTRUCTURE LAYER
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getDocs, getDoc, query, orderBy } from "firebase/firestore";
|
|
1
|
+
import { getDocs, getDoc, query, orderBy, onSnapshot } from "firebase/firestore";
|
|
2
2
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
|
|
4
4
|
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
5
|
+
import type { CreationsSubscriptionCallback, UnsubscribeFunction } from "../../domain/repositories/ICreationsRepository";
|
|
5
6
|
|
|
6
7
|
declare const __DEV__: boolean;
|
|
7
8
|
|
|
@@ -85,10 +86,66 @@ export class CreationsFetcher {
|
|
|
85
86
|
return this.documentMapper(docSnap.id, data);
|
|
86
87
|
} catch (error) {
|
|
87
88
|
if (__DEV__) {
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
console.error("[CreationsRepository] getById() ERROR", error);
|
|
90
91
|
}
|
|
91
92
|
return null;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
|
|
96
|
+
subscribeToAll(
|
|
97
|
+
userId: string,
|
|
98
|
+
onData: CreationsSubscriptionCallback,
|
|
99
|
+
onError?: (error: Error) => void,
|
|
100
|
+
): UnsubscribeFunction {
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.log("[CreationsFetcher] subscribeToAll()", { userId });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
106
|
+
if (!userCollection) {
|
|
107
|
+
onData([]);
|
|
108
|
+
return () => {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const q = query(userCollection, orderBy("createdAt", "desc"));
|
|
112
|
+
|
|
113
|
+
return onSnapshot(
|
|
114
|
+
q,
|
|
115
|
+
(snapshot) => {
|
|
116
|
+
const allCreations = snapshot.docs.map((docSnap) => {
|
|
117
|
+
const data = docSnap.data() as CreationDocument;
|
|
118
|
+
const creation = this.documentMapper(docSnap.id, data);
|
|
119
|
+
|
|
120
|
+
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
121
|
+
const deletedAt =
|
|
122
|
+
data.deletedAt instanceof Date
|
|
123
|
+
? data.deletedAt
|
|
124
|
+
: data.deletedAt &&
|
|
125
|
+
typeof data.deletedAt === "object" &&
|
|
126
|
+
"toDate" in data.deletedAt
|
|
127
|
+
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
128
|
+
: undefined;
|
|
129
|
+
return { ...creation, deletedAt };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return creation;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const filtered = allCreations.filter((c) => !c.deletedAt);
|
|
136
|
+
|
|
137
|
+
if (__DEV__) {
|
|
138
|
+
console.log("[CreationsFetcher] Realtime update:", filtered.length);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onData(filtered);
|
|
142
|
+
},
|
|
143
|
+
(error) => {
|
|
144
|
+
if (__DEV__) {
|
|
145
|
+
console.error("[CreationsFetcher] subscribeToAll() ERROR", error);
|
|
146
|
+
}
|
|
147
|
+
onError?.(error);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
}
|
|
94
151
|
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository.ts - Module loading");
|
|
3
3
|
|
|
4
4
|
import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ICreationsRepository,
|
|
7
|
+
CreationsSubscriptionCallback,
|
|
8
|
+
UnsubscribeFunction,
|
|
9
|
+
} from "../../domain/repositories/ICreationsRepository";
|
|
6
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
7
11
|
import { mapDocumentToCreation } from "../../domain/entities/Creation";
|
|
8
12
|
import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
|
|
@@ -66,6 +70,14 @@ export class CreationsRepository
|
|
|
66
70
|
return this.fetcher.getById(userId, id);
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
subscribeToAll(
|
|
74
|
+
userId: string,
|
|
75
|
+
onData: CreationsSubscriptionCallback,
|
|
76
|
+
onError?: (error: Error) => void,
|
|
77
|
+
): UnsubscribeFunction {
|
|
78
|
+
return this.fetcher.subscribeToAll(userId, onData, onError);
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
async create(userId: string, creation: Creation): Promise<void> {
|
|
70
82
|
return this.writer.create(userId, creation);
|
|
71
83
|
}
|
|
@@ -1,75 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationPersistence Hook
|
|
3
3
|
* Encapsulates Firestore persistence logic for AI generation features
|
|
4
|
-
*
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useCallback, useMemo } from "react";
|
|
8
|
-
import { useQueryClient } from "@umituz/react-native-design-system";
|
|
9
8
|
import { useAuth } from "@umituz/react-native-auth";
|
|
10
9
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
11
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
12
11
|
|
|
13
12
|
declare const __DEV__: boolean;
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Configuration for creation persistence
|
|
17
|
-
*/
|
|
18
14
|
export interface UseCreationPersistenceConfig {
|
|
19
|
-
/** Creation type identifier (e.g., "anime-selfie", "ai-kiss") */
|
|
20
15
|
readonly type: string;
|
|
21
|
-
/** Collection name in Firestore (defaults to "creations") */
|
|
22
16
|
readonly collectionName?: string;
|
|
23
|
-
/** Credit cost for this feature (passed to onCreditDeduct) */
|
|
24
17
|
readonly creditCost?: number;
|
|
25
|
-
/** Callback to deduct credits on successful processing */
|
|
26
18
|
readonly onCreditDeduct?: (cost: number) => Promise<void | boolean>;
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
* Base processing start data - all features must have creationId
|
|
31
|
-
*/
|
|
32
21
|
export interface BaseProcessingStartData {
|
|
33
22
|
readonly creationId: string;
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
/**
|
|
37
|
-
* Base processing result - all features should have creationId
|
|
38
|
-
*/
|
|
39
25
|
export interface BaseProcessingResult {
|
|
40
26
|
readonly creationId?: string;
|
|
41
27
|
readonly imageUrl?: string;
|
|
42
28
|
readonly videoUrl?: string;
|
|
43
29
|
}
|
|
44
30
|
|
|
45
|
-
/**
|
|
46
|
-
* Return type for useCreationPersistence - uses generic callbacks
|
|
47
|
-
*/
|
|
48
31
|
export interface UseCreationPersistenceReturn {
|
|
49
32
|
readonly onProcessingStart: <T extends BaseProcessingStartData>(data: T) => void;
|
|
50
33
|
readonly onProcessingComplete: <T extends BaseProcessingResult>(result: T) => void;
|
|
51
34
|
readonly onError: (error: string, creationId?: string) => void;
|
|
52
35
|
}
|
|
53
36
|
|
|
54
|
-
/**
|
|
55
|
-
* Hook that provides Firestore persistence callbacks for AI features
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* const { deductCredit } = useDeductCredit({ userId, onCreditsExhausted: openPaywall });
|
|
59
|
-
* const persistence = useCreationPersistence({
|
|
60
|
-
* type: "anime-selfie",
|
|
61
|
-
* creditCost: AI_CREDIT_COST.ANIME_SELFIE,
|
|
62
|
-
* onCreditDeduct: async (cost) => {
|
|
63
|
-
* for (let i = 0; i < cost; i++) await deductCredit("image");
|
|
64
|
-
* },
|
|
65
|
-
* });
|
|
66
|
-
*/
|
|
67
37
|
export function useCreationPersistence(
|
|
68
38
|
config: UseCreationPersistenceConfig,
|
|
69
39
|
): UseCreationPersistenceReturn {
|
|
70
40
|
const { type, collectionName = "creations", creditCost, onCreditDeduct } = config;
|
|
71
41
|
const { userId } = useAuth();
|
|
72
|
-
const queryClient = useQueryClient();
|
|
73
42
|
|
|
74
43
|
const repository = useMemo(
|
|
75
44
|
() => createCreationsRepository(collectionName),
|
|
@@ -78,12 +47,14 @@ export function useCreationPersistence(
|
|
|
78
47
|
|
|
79
48
|
const onProcessingStart = useCallback(
|
|
80
49
|
<T extends BaseProcessingStartData>(data: T) => {
|
|
81
|
-
if (__DEV__) {
|
|
50
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
82
51
|
console.log("[useCreationPersistence] onProcessingStart", { type, userId });
|
|
83
52
|
}
|
|
84
53
|
|
|
85
54
|
if (!userId) {
|
|
86
|
-
if (__DEV__
|
|
55
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
+
console.log("[useCreationPersistence] No userId, skipping");
|
|
57
|
+
}
|
|
87
58
|
return;
|
|
88
59
|
}
|
|
89
60
|
|
|
@@ -103,19 +74,18 @@ export function useCreationPersistence(
|
|
|
103
74
|
metadata: cleanMetadata,
|
|
104
75
|
};
|
|
105
76
|
|
|
106
|
-
if (__DEV__) {
|
|
77
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
107
78
|
console.log("[useCreationPersistence] Creating document", { creationId, type });
|
|
108
79
|
}
|
|
109
80
|
|
|
110
81
|
repository.create(userId, creation);
|
|
111
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
112
82
|
},
|
|
113
|
-
[userId, repository,
|
|
83
|
+
[userId, repository, type],
|
|
114
84
|
);
|
|
115
85
|
|
|
116
86
|
const onProcessingComplete = useCallback(
|
|
117
87
|
<T extends BaseProcessingResult>(result: T) => {
|
|
118
|
-
if (__DEV__) {
|
|
88
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
119
89
|
console.log("[useCreationPersistence] onProcessingComplete", {
|
|
120
90
|
creationId: result.creationId,
|
|
121
91
|
hasImageUrl: !!result.imageUrl,
|
|
@@ -124,7 +94,9 @@ export function useCreationPersistence(
|
|
|
124
94
|
}
|
|
125
95
|
|
|
126
96
|
if (!userId || !result.creationId) {
|
|
127
|
-
if (__DEV__
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
console.log("[useCreationPersistence] Missing userId or creationId");
|
|
99
|
+
}
|
|
128
100
|
return;
|
|
129
101
|
}
|
|
130
102
|
|
|
@@ -140,31 +112,31 @@ export function useCreationPersistence(
|
|
|
140
112
|
status: "completed",
|
|
141
113
|
output,
|
|
142
114
|
});
|
|
143
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
144
115
|
|
|
145
|
-
// Deduct credits via callback (app provides implementation)
|
|
146
116
|
if (creditCost && creditCost > 0 && onCreditDeduct) {
|
|
147
|
-
if (__DEV__) {
|
|
117
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
148
118
|
console.log("[useCreationPersistence] Deducting credits", { cost: creditCost });
|
|
149
119
|
}
|
|
150
120
|
onCreditDeduct(creditCost).catch((err) => {
|
|
151
|
-
if (__DEV__) {
|
|
121
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
152
122
|
console.error("[useCreationPersistence] Credit deduction failed", err);
|
|
153
123
|
}
|
|
154
124
|
});
|
|
155
125
|
}
|
|
156
126
|
},
|
|
157
|
-
[userId, repository,
|
|
127
|
+
[userId, repository, creditCost, onCreditDeduct],
|
|
158
128
|
);
|
|
159
129
|
|
|
160
130
|
const onError = useCallback(
|
|
161
131
|
(error: string, creationId?: string) => {
|
|
162
|
-
if (__DEV__) {
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
163
133
|
console.log("[useCreationPersistence] onError", { error, creationId });
|
|
164
134
|
}
|
|
165
135
|
|
|
166
136
|
if (!userId || !creationId) {
|
|
167
|
-
if (__DEV__
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
+
console.log("[useCreationPersistence] Missing userId or creationId");
|
|
139
|
+
}
|
|
168
140
|
return;
|
|
169
141
|
}
|
|
170
142
|
|
|
@@ -172,9 +144,8 @@ export function useCreationPersistence(
|
|
|
172
144
|
status: "failed",
|
|
173
145
|
metadata: { error },
|
|
174
146
|
});
|
|
175
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
176
147
|
},
|
|
177
|
-
[userId, repository
|
|
148
|
+
[userId, repository],
|
|
178
149
|
);
|
|
179
150
|
|
|
180
151
|
return useMemo(
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationRating Hook
|
|
3
|
-
* Handles rating of creations
|
|
3
|
+
* Handles rating of creations
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
9
11
|
|
|
10
12
|
interface UseCreationRatingProps {
|
|
11
13
|
readonly userId: string | null;
|
|
@@ -15,41 +17,53 @@ interface UseCreationRatingProps {
|
|
|
15
17
|
interface RatingVariables {
|
|
16
18
|
readonly id: string;
|
|
17
19
|
readonly rating: number;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UseCreationRatingReturn {
|
|
24
|
+
readonly mutate: (variables: RatingVariables) => void;
|
|
25
|
+
readonly mutateAsync: (variables: RatingVariables) => Promise<boolean>;
|
|
26
|
+
readonly isPending: boolean;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export function useCreationRating({
|
|
21
30
|
userId,
|
|
22
31
|
repository,
|
|
23
|
-
}: UseCreationRatingProps) {
|
|
24
|
-
const
|
|
25
|
-
const queryKey = ["creations", userId ?? ""];
|
|
32
|
+
}: UseCreationRatingProps): UseCreationRatingReturn {
|
|
33
|
+
const [isPending, setIsPending] = useState(false);
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const mutateAsync = useCallback(
|
|
36
|
+
async ({ id, rating, description }: RatingVariables): Promise<boolean> => {
|
|
29
37
|
if (!userId) return false;
|
|
30
|
-
return repository.rate(userId, id, rating);
|
|
31
|
-
},
|
|
32
|
-
onMutate: async ({ id, rating }: RatingVariables) => {
|
|
33
|
-
await queryClient.cancelQueries({ queryKey });
|
|
34
|
-
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
35
|
-
|
|
36
|
-
if (previousData) {
|
|
37
|
-
queryClient.setQueryData<Creation[]>(queryKey, (old) =>
|
|
38
|
-
old?.map((c) =>
|
|
39
|
-
c.id === id ? { ...c, rating, ratedAt: new Date() } : c
|
|
40
|
-
) ?? []
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
setIsPending(true);
|
|
40
|
+
try {
|
|
41
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
42
|
+
console.log("[useCreationRating] Rating:", { id, rating });
|
|
43
|
+
}
|
|
44
|
+
const result = await repository.rate(userId, id, rating, description);
|
|
45
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
46
|
+
console.log("[useCreationRating] Rate result:", result);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
+
console.error("[useCreationRating] Error:", error);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
} finally {
|
|
55
|
+
setIsPending(false);
|
|
49
56
|
}
|
|
50
57
|
},
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
[userId, repository],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const mutate = useCallback(
|
|
62
|
+
(variables: RatingVariables): void => {
|
|
63
|
+
void mutateAsync(variables);
|
|
53
64
|
},
|
|
54
|
-
|
|
65
|
+
[mutateAsync],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return { mutate, mutateAsync, isPending };
|
|
55
69
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreations Hook
|
|
3
|
-
*
|
|
3
|
+
* Realtime Firestore listener for user's creations
|
|
4
|
+
* Auto-updates UI when Firestore data changes
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
9
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
staleTime: 5 * 60 * 1000, // 5 minutes - use cache invalidation on mutations
|
|
12
|
-
gcTime: 30 * 60 * 1000,
|
|
13
|
-
};
|
|
11
|
+
declare const __DEV__: boolean;
|
|
14
12
|
|
|
15
13
|
interface UseCreationsProps {
|
|
16
14
|
readonly userId: string | null;
|
|
@@ -18,21 +16,68 @@ interface UseCreationsProps {
|
|
|
18
16
|
readonly enabled?: boolean;
|
|
19
17
|
}
|
|
20
18
|
|
|
19
|
+
interface UseCreationsReturn {
|
|
20
|
+
readonly data: Creation[] | undefined;
|
|
21
|
+
readonly isLoading: boolean;
|
|
22
|
+
readonly error: Error | null;
|
|
23
|
+
readonly refetch: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function useCreations({
|
|
22
27
|
userId,
|
|
23
28
|
repository,
|
|
24
29
|
enabled = true,
|
|
25
|
-
}: UseCreationsProps) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
}: UseCreationsProps): UseCreationsReturn {
|
|
31
|
+
const [data, setData] = useState<Creation[] | undefined>(undefined);
|
|
32
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState<Error | null>(null);
|
|
34
|
+
|
|
35
|
+
const refetch = useCallback(() => {
|
|
36
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
37
|
+
console.log("[useCreations] refetch() - realtime listener handles updates");
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!userId || !enabled) {
|
|
43
|
+
setData([]);
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
49
|
+
console.log("[useCreations] Setting up realtime listener", { userId });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
|
|
55
|
+
const unsubscribe = repository.subscribeToAll(
|
|
56
|
+
userId,
|
|
57
|
+
(creations) => {
|
|
58
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
59
|
+
console.log("[useCreations] Realtime update:", creations.length);
|
|
60
|
+
}
|
|
61
|
+
setData(creations);
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
setError(null);
|
|
64
|
+
},
|
|
65
|
+
(err) => {
|
|
66
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
+
console.error("[useCreations] Realtime listener error:", err);
|
|
68
|
+
}
|
|
69
|
+
setError(err);
|
|
70
|
+
setIsLoading(false);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
+
console.log("[useCreations] Cleaning up realtime listener");
|
|
31
77
|
}
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
78
|
+
unsubscribe();
|
|
79
|
+
};
|
|
80
|
+
}, [userId, repository, enabled]);
|
|
81
|
+
|
|
82
|
+
return { data, isLoading, error, refetch };
|
|
38
83
|
}
|
|
@@ -1,48 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useDeleteCreation Hook
|
|
3
|
-
* Handles deletion of user creations
|
|
3
|
+
* Handles deletion of user creations
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
9
11
|
|
|
10
12
|
interface UseDeleteCreationProps {
|
|
11
13
|
readonly userId: string | null;
|
|
12
14
|
readonly repository: ICreationsRepository;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
interface UseDeleteCreationReturn {
|
|
18
|
+
readonly mutate: (creationId: string) => void;
|
|
19
|
+
readonly mutateAsync: (creationId: string) => Promise<boolean>;
|
|
20
|
+
readonly isPending: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export function useDeleteCreation({
|
|
16
24
|
userId,
|
|
17
25
|
repository,
|
|
18
|
-
}: UseDeleteCreationProps) {
|
|
19
|
-
const
|
|
20
|
-
const queryKey = ["creations", userId ?? ""];
|
|
26
|
+
}: UseDeleteCreationProps): UseDeleteCreationReturn {
|
|
27
|
+
const [isPending, setIsPending] = useState(false);
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
const mutateAsync = useCallback(
|
|
30
|
+
async (creationId: string): Promise<boolean> => {
|
|
24
31
|
if (!userId) return false;
|
|
25
|
-
return repository.delete(userId, creationId);
|
|
26
|
-
},
|
|
27
|
-
onMutate: async (creationId: string) => {
|
|
28
|
-
await queryClient.cancelQueries({ queryKey });
|
|
29
|
-
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
30
|
-
|
|
31
|
-
if (previousData) {
|
|
32
|
-
queryClient.setQueryData<Creation[]>(queryKey, (old: Creation[] | undefined) =>
|
|
33
|
-
old?.filter((c: Creation) => c.id !== creationId) ?? []
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
setIsPending(true);
|
|
34
|
+
try {
|
|
35
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
36
|
+
console.log("[useDeleteCreation] Deleting:", creationId);
|
|
37
|
+
}
|
|
38
|
+
const result = await repository.delete(userId, creationId);
|
|
39
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
40
|
+
console.log("[useDeleteCreation] Delete result:", result);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
+
console.error("[useDeleteCreation] Error:", error);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
} finally {
|
|
49
|
+
setIsPending(false);
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
[userId, repository],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const mutate = useCallback(
|
|
56
|
+
(creationId: string): void => {
|
|
57
|
+
void mutateAsync(creationId);
|
|
46
58
|
},
|
|
47
|
-
|
|
59
|
+
[mutateAsync],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { mutate, mutateAsync, isPending };
|
|
48
63
|
}
|