@umituz/react-native-ai-generation-content 1.12.11 → 1.12.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.12.11",
3
+ "version": "1.12.14",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -78,4 +78,4 @@
78
78
  "publishConfig": {
79
79
  "access": "public"
80
80
  }
81
- }
81
+ }
@@ -12,6 +12,7 @@ export interface Creation {
12
12
  readonly originalUri?: string;
13
13
  readonly createdAt: Date;
14
14
  readonly isShared: boolean;
15
+ readonly isFavorite: boolean;
15
16
  }
16
17
 
17
18
  export interface CreationDocument {
@@ -26,6 +27,7 @@ export interface CreationDocument {
26
27
  readonly type?: string;
27
28
  readonly status?: string;
28
29
  readonly isShared: boolean;
30
+ readonly isFavorite: boolean;
29
31
  readonly createdAt: FirebaseTimestamp | Date; // Allow Date for writing
30
32
  readonly completedAt?: FirebaseTimestamp | Date;
31
33
  }
@@ -47,5 +49,6 @@ export function mapDocumentToCreation(
47
49
  originalUri: data.originalImageUrl || data.originalImage,
48
50
  createdAt: (data.createdAt as any)?.toDate?.() || (data.createdAt instanceof Date ? data.createdAt : new Date()),
49
51
  isShared: data.isShared ?? false,
52
+ isFavorite: data.isFavorite ?? false,
50
53
  };
51
54
  }
@@ -15,6 +15,8 @@ export interface ICreationsRepository {
15
15
  updates: Partial<Creation>,
16
16
  ): Promise<boolean>;
17
17
  delete(userId: string, creationId: string): Promise<boolean>;
18
+ deleteMultiple(userId: string, creationIds: string[]): Promise<boolean>;
19
+ toggleFavorite(userId: string, creationId: string): Promise<boolean>;
18
20
  updateShared(
19
21
  userId: string,
20
22
  creationId: string,
@@ -52,6 +52,7 @@ export interface CreationsConfig {
52
52
  readonly gridColumns?: number;
53
53
  readonly pathBuilder?: PathBuilder;
54
54
  readonly documentMapper?: DocumentMapper;
55
+ readonly enableFiltering?: boolean;
55
56
  }
56
57
 
57
58
  export const DEFAULT_TRANSLATIONS: CreationsTranslations = {
@@ -157,6 +157,7 @@ export class CreationsRepository
157
157
  createdAt: creation.createdAt,
158
158
  metadata: creation.metadata || {},
159
159
  isShared: creation.isShared || false,
160
+ isFavorite: creation.isFavorite || false,
160
161
  };
161
162
 
162
163
  await setDoc(docRef, data);
@@ -183,6 +184,9 @@ export class CreationsRepository
183
184
  if (updates.isShared !== undefined) {
184
185
  updateData.isShared = updates.isShared;
185
186
  }
187
+ if (updates.isFavorite !== undefined) {
188
+ updateData.isFavorite = updates.isFavorite;
189
+ }
186
190
  if (updates.uri !== undefined) {
187
191
  updateData.uri = updates.uri;
188
192
  }
@@ -215,6 +219,32 @@ export class CreationsRepository
215
219
  }
216
220
  }
217
221
 
222
+ async deleteMultiple(userId: string, creationIds: string[]): Promise<boolean> {
223
+ try {
224
+ const promises = creationIds.map((id) => this.delete(userId, id));
225
+ const results = await Promise.all(promises);
226
+ return results.every((r) => r);
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ async toggleFavorite(userId: string, creationId: string): Promise<boolean> {
233
+ const docRef = this.getDocRef(userId, creationId);
234
+ if (!docRef) return false;
235
+
236
+ try {
237
+ const docSnap = await getDoc(docRef);
238
+ if (!docSnap.exists()) return false;
239
+
240
+ const current = docSnap.data() as CreationDocument;
241
+ await updateDoc(docRef, { isFavorite: !current.isFavorite });
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
218
248
  async updateShared(
219
249
  userId: string,
220
250
  creationId: string,
@@ -19,6 +19,10 @@ interface CreationCardProps {
19
19
  readonly onView?: (creation: Creation) => void;
20
20
  readonly onShare: (creation: Creation) => void;
21
21
  readonly onDelete: (creation: Creation) => void;
22
+ readonly onToggleFavorite?: (creation: Creation) => void;
23
+ readonly isSelected?: boolean;
24
+ readonly onSelect?: (creation: Creation) => void;
25
+ readonly isSelectionMode?: boolean;
22
26
  }
23
27
 
24
28
  export function CreationCard({
@@ -27,6 +31,10 @@ export function CreationCard({
27
31
  onView,
28
32
  onShare,
29
33
  onDelete,
34
+ onToggleFavorite,
35
+ isSelected,
36
+ onSelect,
37
+ isSelectionMode,
30
38
  }: CreationCardProps) {
31
39
  const tokens = useAppDesignTokens();
32
40
 
@@ -40,6 +48,14 @@ export function CreationCard({
40
48
  () => onDelete(creation),
41
49
  [creation, onDelete],
42
50
  );
51
+ const handleToggleFavorite = useCallback(
52
+ () => onToggleFavorite?.(creation),
53
+ [creation, onToggleFavorite],
54
+ );
55
+ const handleSelect = useCallback(
56
+ () => onSelect?.(creation),
57
+ [creation, onSelect],
58
+ );
43
59
 
44
60
  const formattedDate = useMemo(() => {
45
61
  const date =
@@ -63,11 +79,42 @@ export function CreationCard({
63
79
  borderRadius: tokens.spacing.md,
64
80
  overflow: "hidden",
65
81
  marginBottom: tokens.spacing.md,
82
+ borderWidth: 1,
83
+ borderColor: isSelected ? tokens.colors.primary : "transparent",
84
+ },
85
+ imageContainer: {
86
+ width: 100,
87
+ height: 100,
88
+ position: "relative",
66
89
  },
67
90
  thumbnail: {
68
91
  width: 100,
69
92
  height: 100,
70
93
  },
94
+ favoriteBtn: {
95
+ position: "absolute",
96
+ top: 4,
97
+ right: 4,
98
+ width: 28,
99
+ height: 28,
100
+ borderRadius: 14,
101
+ backgroundColor: "rgba(255,255,255,0.7)",
102
+ justifyContent: "center",
103
+ alignItems: "center",
104
+ },
105
+ selectionOverlay: {
106
+ position: "absolute",
107
+ top: 4,
108
+ left: 4,
109
+ width: 24,
110
+ height: 24,
111
+ borderRadius: 12,
112
+ backgroundColor: isSelected ? tokens.colors.primary : "rgba(255,255,255,0.5)",
113
+ justifyContent: "center",
114
+ alignItems: "center",
115
+ borderWidth: 1,
116
+ borderColor: tokens.colors.primary,
117
+ },
71
118
  content: {
72
119
  flex: 1,
73
120
  padding: tokens.spacing.md,
@@ -103,12 +150,33 @@ export function CreationCard({
103
150
  alignItems: "center",
104
151
  },
105
152
  }),
106
- [tokens],
153
+ [tokens, isSelected],
107
154
  );
108
155
 
109
156
  return (
110
- <View style={styles.container}>
111
- <Image source={{ uri: creation.uri }} style={styles.thumbnail} />
157
+ <TouchableOpacity
158
+ activeOpacity={0.9}
159
+ style={styles.container}
160
+ onPress={isSelectionMode ? handleSelect : handleView}
161
+ onLongPress={handleSelect}
162
+ >
163
+ <View style={styles.imageContainer}>
164
+ <Image source={{ uri: creation.uri }} style={styles.thumbnail} />
165
+ {isSelectionMode && (
166
+ <View style={styles.selectionOverlay}>
167
+ {isSelected && <AtomicIcon name="checkmark-circle" size="xs" color="white" />}
168
+ </View>
169
+ )}
170
+ {!isSelectionMode && onToggleFavorite && (
171
+ <TouchableOpacity style={styles.favoriteBtn} onPress={handleToggleFavorite}>
172
+ <AtomicIcon
173
+ name={creation.isFavorite ? "heart" : "heart-outline"}
174
+ size="xs"
175
+ color={creation.isFavorite ? "error" : "secondary"}
176
+ />
177
+ </TouchableOpacity>
178
+ )}
179
+ </View>
112
180
  <View style={styles.content}>
113
181
  <View>
114
182
  <View style={styles.typeRow}>
@@ -117,20 +185,17 @@ export function CreationCard({
117
185
  </View>
118
186
  <AtomicText style={styles.dateText}>{formattedDate}</AtomicText>
119
187
  </View>
120
- <View style={styles.actions}>
121
- {onView && (
122
- <TouchableOpacity style={styles.actionBtn} onPress={handleView}>
123
- <AtomicIcon name="eye" size="sm" color="primary" />
188
+ {!isSelectionMode && (
189
+ <View style={styles.actions}>
190
+ <TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
191
+ <AtomicIcon name="share-social" size="sm" color="primary" />
124
192
  </TouchableOpacity>
125
- )}
126
- <TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
127
- <AtomicIcon name="share-social" size="sm" color="primary" />
128
- </TouchableOpacity>
129
- <TouchableOpacity style={styles.actionBtn} onPress={handleDelete}>
130
- <AtomicIcon name="trash" size="sm" color="error" />
131
- </TouchableOpacity>
132
- </View>
193
+ <TouchableOpacity style={styles.actionBtn} onPress={handleDelete}>
194
+ <AtomicIcon name="trash" size="sm" color="error" />
195
+ </TouchableOpacity>
196
+ </View>
197
+ )}
133
198
  </View>
134
- </View>
199
+ </TouchableOpacity>
135
200
  );
136
201
  }
@@ -13,6 +13,10 @@ interface CreationsGridProps {
13
13
  readonly onView: (creation: Creation) => void;
14
14
  readonly onShare: (creation: Creation) => void;
15
15
  readonly onDelete: (creation: Creation) => void;
16
+ readonly onToggleFavorite?: (creation: Creation) => void;
17
+ readonly selectedIds?: string[];
18
+ readonly onSelect?: (creation: Creation) => void;
19
+ readonly isSelectionMode?: boolean;
16
20
  readonly contentContainerStyle?: any;
17
21
  readonly ListEmptyComponent?: React.ReactElement | null;
18
22
  readonly ListHeaderComponent?: React.ComponentType<any> | React.ReactElement | null;
@@ -26,6 +30,10 @@ export const CreationsGrid: React.FC<CreationsGridProps> = ({
26
30
  onView,
27
31
  onShare,
28
32
  onDelete,
33
+ onToggleFavorite,
34
+ selectedIds = [],
35
+ onSelect,
36
+ isSelectionMode = false,
29
37
  contentContainerStyle,
30
38
  ListEmptyComponent,
31
39
  ListHeaderComponent,
@@ -40,6 +48,10 @@ export const CreationsGrid: React.FC<CreationsGridProps> = ({
40
48
  onView={() => onView(item)}
41
49
  onShare={() => onShare(item)}
42
50
  onDelete={() => onDelete(item)}
51
+ onToggleFavorite={onToggleFavorite}
52
+ isSelected={selectedIds.includes(item.id)}
53
+ onSelect={onSelect}
54
+ isSelectionMode={isSelectionMode}
43
55
  />
44
56
  );
45
57
 
@@ -10,7 +10,12 @@ interface GalleryHeaderProps {
10
10
  readonly onFilterPress: () => void;
11
11
  readonly filterLabel?: string;
12
12
  readonly filterIcon?: any;
13
+ readonly onFavoritesPress?: () => void;
14
+ readonly showOnlyFavorites?: boolean;
13
15
  readonly style?: any;
16
+ readonly subtitle?: string;
17
+ readonly isFilterEnabled?: boolean;
18
+ readonly showCount?: boolean;
14
19
  }
15
20
 
16
21
  export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
@@ -21,6 +26,11 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
21
26
  onFilterPress,
22
27
  filterLabel = 'Filter',
23
28
  filterIcon = 'filter-outline',
29
+ onFavoritesPress,
30
+ showOnlyFavorites = false,
31
+ subtitle,
32
+ isFilterEnabled = true,
33
+ showCount = true,
24
34
  style,
25
35
  }) => {
26
36
  const tokens = useAppDesignTokens();
@@ -28,29 +38,46 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
28
38
 
29
39
  return (
30
40
  <View style={[styles.headerArea, style]}>
31
- <View>
32
- <AtomicText style={styles.title}>{title}</AtomicText>
41
+ <View style={styles.titleArea}>
42
+ {!!title && <AtomicText style={styles.title}>{title}</AtomicText>}
33
43
  <AtomicText style={styles.subtitle}>
34
- {count} {countLabel}
44
+ {subtitle || (showCount ? `${count} ${countLabel}` : '')}
35
45
  </AtomicText>
36
46
  </View>
37
- <TouchableOpacity
38
- onPress={onFilterPress}
39
- style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
40
- activeOpacity={0.7}
41
- >
42
- <AtomicIcon
43
- name={filterIcon}
44
- size="sm"
45
- color={isFiltered ? "primary" : "secondary"}
46
- />
47
- <AtomicText style={[styles.filterText, { color: isFiltered ? tokens.colors.primary : tokens.colors.textSecondary }]}>
48
- {filterLabel}
49
- </AtomicText>
50
- {isFiltered && (
51
- <View style={styles.badge} />
47
+ <View style={styles.actions}>
48
+ {onFavoritesPress && (
49
+ <TouchableOpacity
50
+ onPress={onFavoritesPress}
51
+ style={[styles.actionButton, showOnlyFavorites && styles.actionButtonActive]}
52
+ activeOpacity={0.7}
53
+ >
54
+ <AtomicIcon
55
+ name={showOnlyFavorites ? "heart" : "heart-outline"}
56
+ size="sm"
57
+ color={showOnlyFavorites ? "error" : "secondary"}
58
+ />
59
+ </TouchableOpacity>
60
+ )}
61
+ {isFilterEnabled && (
62
+ <TouchableOpacity
63
+ onPress={onFilterPress}
64
+ style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
65
+ activeOpacity={0.7}
66
+ >
67
+ <AtomicIcon
68
+ name={filterIcon}
69
+ size="sm"
70
+ color={isFiltered ? "primary" : "secondary"}
71
+ />
72
+ <AtomicText style={[styles.filterText, { color: isFiltered ? tokens.colors.primary : tokens.colors.textSecondary }]}>
73
+ {filterLabel}
74
+ </AtomicText>
75
+ {isFiltered && (
76
+ <View style={styles.badge} />
77
+ )}
78
+ </TouchableOpacity>
52
79
  )}
53
- </TouchableOpacity>
80
+ </View>
54
81
  </View>
55
82
  );
56
83
  };
@@ -64,6 +91,10 @@ const useStyles = (tokens: any) => StyleSheet.create({
64
91
  paddingVertical: tokens.spacing.sm,
65
92
  marginBottom: tokens.spacing.sm,
66
93
  },
94
+ titleArea: {
95
+ flex: 1,
96
+ marginRight: tokens.spacing.md,
97
+ },
67
98
  title: {
68
99
  fontSize: 20,
69
100
  fontWeight: "700",
@@ -75,6 +106,25 @@ const useStyles = (tokens: any) => StyleSheet.create({
75
106
  color: tokens.colors.textSecondary,
76
107
  opacity: 0.6
77
108
  },
109
+ actions: {
110
+ flexDirection: 'row',
111
+ alignItems: 'center',
112
+ gap: tokens.spacing.sm,
113
+ },
114
+ actionButton: {
115
+ width: 40,
116
+ height: 40,
117
+ borderRadius: 20,
118
+ backgroundColor: tokens.colors.surfaceVariant,
119
+ justifyContent: 'center',
120
+ alignItems: 'center',
121
+ borderWidth: 1,
122
+ borderColor: 'transparent',
123
+ },
124
+ actionButtonActive: {
125
+ backgroundColor: tokens.colors.primary + "15",
126
+ borderColor: tokens.colors.primary + "30",
127
+ },
78
128
  filterButton: {
79
129
  flexDirection: 'row',
80
130
  alignItems: 'center',
@@ -85,6 +135,7 @@ const useStyles = (tokens: any) => StyleSheet.create({
85
135
  backgroundColor: tokens.colors.surfaceVariant,
86
136
  borderWidth: 1,
87
137
  borderColor: 'transparent',
138
+ height: 40,
88
139
  },
89
140
  filterButtonActive: {
90
141
  backgroundColor: tokens.colors.primary + "15",
@@ -5,3 +5,5 @@
5
5
  export { useCreations } from "./useCreations";
6
6
  export { useDeleteCreation } from "./useDeleteCreation";
7
7
  export { useCreationsFilter } from "./useCreationsFilter";
8
+ export { useToggleFavorite } from "./useToggleFavorite";
9
+ export { useDeleteMultipleCreations } from "./useDeleteMultipleCreations";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * useCreationsFilter Hook
3
- * Handles filtering of creations by type
3
+ * Handles filtering of creations by type, search, and favorites
4
4
  */
5
5
 
6
6
  import { useState, useMemo, useCallback } from "react";
@@ -16,41 +16,55 @@ export function useCreationsFilter({
16
16
  defaultFilterId = "all"
17
17
  }: UseCreationsFilterProps) {
18
18
  const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
19
+ const [searchQuery, setSearchQuery] = useState("");
20
+ const [showOnlyFavorites, setShowOnlyFavorites] = useState(false);
19
21
 
20
22
  const filtered = useMemo(() => {
21
23
  if (!creations) return [];
22
- if (selectedIds.includes(defaultFilterId)) return creations;
23
24
 
24
- return creations.filter((c) =>
25
- selectedIds.includes(c.type) ||
26
- selectedIds.some(id => (c as any).metadata?.tags?.includes(id))
27
- );
28
- }, [creations, selectedIds, defaultFilterId]);
25
+ let result = creations;
26
+
27
+ // 1. Filter by Favorites
28
+ if (showOnlyFavorites) {
29
+ result = result.filter(c => c.isFavorite);
30
+ }
31
+
32
+ // 2. Filter by Category/Type
33
+ if (!selectedIds.includes(defaultFilterId)) {
34
+ result = result.filter((c) =>
35
+ selectedIds.includes(c.type) ||
36
+ selectedIds.some(id => (c as any).metadata?.tags?.includes(id))
37
+ );
38
+ }
39
+
40
+ // 3. Filter by Search Query
41
+ if (searchQuery.trim()) {
42
+ const query = searchQuery.toLowerCase();
43
+ result = result.filter(c =>
44
+ c.prompt?.toLowerCase().includes(query) ||
45
+ c.type.toLowerCase().includes(query)
46
+ );
47
+ }
48
+
49
+ return result;
50
+ }, [creations, selectedIds, defaultFilterId, searchQuery, showOnlyFavorites]);
29
51
 
30
52
  const toggleFilter = useCallback((filterId: string, multiSelect: boolean = false) => {
31
53
  setSelectedIds(prev => {
32
- // If selecting 'all', clear everything else
33
54
  if (filterId === defaultFilterId) return [defaultFilterId];
34
55
 
35
56
  let newIds: string[];
36
57
  if (!multiSelect) {
37
- // Single select
38
- // If we tap the already selected item in single mode, should we Deselect it?
39
- // Typically in radio-button style filters, no. But let's assume valid toggling behavior suitable for the UI.
40
- // If single select, simply switch to the new one.
41
58
  if (prev.includes(filterId) && prev.length === 1) return prev;
42
59
  newIds = [filterId];
43
60
  } else {
44
- // Multi select
45
61
  if (prev.includes(filterId)) {
46
62
  newIds = prev.filter(id => id !== filterId);
47
63
  } else {
48
- // Remove 'all' if present
49
64
  newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
50
65
  }
51
66
  }
52
67
 
53
- // If nothing selected, revert to 'all'
54
68
  if (newIds.length === 0) return [defaultFilterId];
55
69
  return newIds;
56
70
  });
@@ -58,13 +72,19 @@ export function useCreationsFilter({
58
72
 
59
73
  const clearFilters = useCallback(() => {
60
74
  setSelectedIds([defaultFilterId]);
75
+ setSearchQuery("");
76
+ setShowOnlyFavorites(false);
61
77
  }, [defaultFilterId]);
62
78
 
63
79
  return {
64
80
  filtered,
65
81
  selectedIds,
82
+ searchQuery,
83
+ setSearchQuery,
84
+ showOnlyFavorites,
85
+ setShowOnlyFavorites,
66
86
  toggleFilter,
67
87
  clearFilters,
68
- isFiltered: !selectedIds.includes(defaultFilterId),
88
+ isFiltered: !selectedIds.includes(defaultFilterId) || !!searchQuery || showOnlyFavorites,
69
89
  };
70
90
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * useDeleteMultipleCreations Hook
3
+ * Handles batch deletion of user creations with optimistic update
4
+ */
5
+
6
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
7
+ import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
8
+ import type { Creation } from "../../domain/entities/Creation";
9
+
10
+ interface UseDeleteMultipleCreationsProps {
11
+ readonly userId: string | null;
12
+ readonly repository: ICreationsRepository;
13
+ }
14
+
15
+ export function useDeleteMultipleCreations({
16
+ userId,
17
+ repository,
18
+ }: UseDeleteMultipleCreationsProps) {
19
+ const queryClient = useQueryClient();
20
+
21
+ return useMutation({
22
+ mutationFn: async (creationIds: string[]) => {
23
+ if (!userId) return false;
24
+ return repository.deleteMultiple(userId, creationIds);
25
+ },
26
+ onMutate: async (creationIds) => {
27
+ if (!userId) return;
28
+
29
+ await queryClient.cancelQueries({
30
+ queryKey: ["creations", userId],
31
+ });
32
+
33
+ const previous = queryClient.getQueryData<Creation[]>([
34
+ "creations",
35
+ userId,
36
+ ]);
37
+
38
+ queryClient.setQueryData<Creation[]>(
39
+ ["creations", userId],
40
+ (old) =>
41
+ old?.filter((c) => !creationIds.includes(c.id)) ?? [],
42
+ );
43
+
44
+ return { previous };
45
+ },
46
+ onError: (_err, _ids, rollback) => {
47
+ if (userId && rollback?.previous) {
48
+ queryClient.setQueryData(["creations", userId], rollback.previous);
49
+ }
50
+ },
51
+ onSettled: () => {
52
+ if (userId) {
53
+ queryClient.invalidateQueries({ queryKey: ["creations", userId] });
54
+ }
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * useToggleFavorite Hook
3
+ * Handles favoriting/unfavoriting of user creations with optimistic update
4
+ */
5
+
6
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
7
+ import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
8
+ import type { Creation } from "../../domain/entities/Creation";
9
+
10
+ interface UseToggleFavoriteProps {
11
+ readonly userId: string | null;
12
+ readonly repository: ICreationsRepository;
13
+ }
14
+
15
+ export function useToggleFavorite({
16
+ userId,
17
+ repository,
18
+ }: UseToggleFavoriteProps) {
19
+ const queryClient = useQueryClient();
20
+
21
+ return useMutation({
22
+ mutationFn: async (creationId: string) => {
23
+ if (!userId) return false;
24
+ return repository.toggleFavorite(userId, creationId);
25
+ },
26
+ onMutate: async (creationId) => {
27
+ if (!userId) return;
28
+
29
+ await queryClient.cancelQueries({
30
+ queryKey: ["creations", userId],
31
+ });
32
+
33
+ const previous = queryClient.getQueryData<Creation[]>([
34
+ "creations",
35
+ userId,
36
+ ]);
37
+
38
+ queryClient.setQueryData<Creation[]>(
39
+ ["creations", userId],
40
+ (old) =>
41
+ old?.map((c) =>
42
+ c.id === creationId ? { ...c, isFavorite: !c.isFavorite } : c,
43
+ ) ?? [],
44
+ );
45
+
46
+ return { previous };
47
+ },
48
+ onError: (_err, _id, rollback) => {
49
+ if (userId && rollback?.previous) {
50
+ queryClient.setQueryData(["creations", userId], rollback.previous);
51
+ }
52
+ },
53
+ onSettled: () => {
54
+ if (userId) {
55
+ queryClient.invalidateQueries({ queryKey: ["creations", userId] });
56
+ }
57
+ },
58
+ });
59
+ }
@@ -1,29 +1,45 @@
1
1
  import React, { useMemo, useCallback, useState } from "react";
2
- import { View, StyleSheet, ActivityIndicator } from "react-native";
3
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
4
- import { useSharing } from "@umituz/react-native-design-system";
5
- import { useSafeAreaInsets } from "react-native-safe-area-context";
2
+ import { View, StyleSheet } from "react-native";
3
+ import {
4
+ useAppDesignTokens,
5
+ useSharing,
6
+ ScreenLayout,
7
+ ScreenHeader,
8
+ useAlert,
9
+ AlertMode,
10
+ type BottomSheetModalRef
11
+ } from "@umituz/react-native-design-system";
6
12
  import { useFocusEffect } from "@react-navigation/native";
7
13
  import { useCreations } from "../hooks/useCreations";
8
14
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
9
15
  import { useCreationsFilter } from "../hooks/useCreationsFilter";
10
- import { useAlert, AlertMode, type BottomSheetModalRef, type FilterCategory } from "@umituz/react-native-design-system";
11
- import { GalleryHeader, CreationsGrid, FilterBottomSheet, CreationImageViewer, CreationsGalleryEmptyState } from "../components";
16
+ import { useToggleFavorite } from "../hooks/useToggleFavorite";
17
+ import { useDeleteMultipleCreations } from "../hooks/useDeleteMultipleCreations";
18
+ import {
19
+ GalleryHeader,
20
+ CreationsGrid,
21
+ FilterBottomSheet,
22
+ CreationImageViewer,
23
+ CreationsGalleryEmptyState
24
+ } from "../components";
25
+ import { SearchBar } from "@umituz/react-native-design-system";
12
26
  import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
13
27
  import type { Creation } from "../../domain/entities/Creation";
14
28
  import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
15
29
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
16
- import { CreationDetailScreen } from "./CreationDetailScreen";
17
30
 
18
31
  interface CreationsGalleryScreenProps {
19
32
  readonly userId: string | null;
20
33
  readonly repository: ICreationsRepository;
21
34
  readonly config: CreationsConfig;
22
- readonly t: (key: string) => string;
35
+ readonly t: (key: string, options?: any) => string;
23
36
  readonly enableEditing?: boolean;
24
37
  readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
25
38
  readonly onEmptyAction?: () => void;
26
39
  readonly emptyActionLabel?: string;
40
+ readonly onBackPress?: () => void;
41
+ readonly headerTitle?: string;
42
+ readonly showCount?: boolean;
27
43
  }
28
44
 
29
45
  export function CreationsGalleryScreen({
@@ -35,9 +51,11 @@ export function CreationsGalleryScreen({
35
51
  onImageEdit,
36
52
  onEmptyAction,
37
53
  emptyActionLabel,
54
+ onBackPress,
55
+ headerTitle,
56
+ showCount = true,
38
57
  }: CreationsGalleryScreenProps) {
39
58
  const tokens = useAppDesignTokens();
40
- const insets = useSafeAreaInsets();
41
59
  const { share } = useSharing();
42
60
  const alert = useAlert();
43
61
 
@@ -48,8 +66,27 @@ export function CreationsGalleryScreen({
48
66
 
49
67
  const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
50
68
  const creations = creationsData as Creation[] | undefined;
69
+
51
70
  const deleteMutation = useDeleteCreation({ userId, repository });
52
- const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
71
+ const toggleFavoriteMutation = useToggleFavorite({ userId, repository });
72
+ const deleteMultipleMutation = useDeleteMultipleCreations({ userId, repository });
73
+
74
+ const {
75
+ filtered,
76
+ selectedIds: selectedCategoryIds,
77
+ searchQuery,
78
+ setSearchQuery,
79
+ showOnlyFavorites,
80
+ setShowOnlyFavorites,
81
+ toggleFilter,
82
+ clearFilters,
83
+ isFiltered
84
+ } = useCreationsFilter({ creations });
85
+
86
+ const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
87
+ const [isSelectionMode, setIsSelectionMode] = useState(false);
88
+
89
+
53
90
 
54
91
  // Refetch creations when screen comes into focus
55
92
  useFocusEffect(
@@ -85,37 +122,96 @@ export function CreationsGalleryScreen({
85
122
  );
86
123
  }, [alert, config, deleteMutation, t]);
87
124
 
125
+ const handleToggleFavorite = useCallback((creation: Creation) => {
126
+ toggleFavoriteMutation.mutate(creation.id);
127
+ }, [toggleFavoriteMutation]);
128
+
129
+ const toggleSelection = useCallback((creation: Creation) => {
130
+ setSelectedItemIds(prev => {
131
+ const isSelected = prev.includes(creation.id);
132
+ const next = isSelected ? prev.filter(id => id !== creation.id) : [...prev, creation.id];
133
+ if (next.length === 0) setIsSelectionMode(false);
134
+ else setIsSelectionMode(true);
135
+ return next;
136
+ });
137
+ }, []);
138
+
139
+ const handleDeleteSelected = useCallback(() => {
140
+ if (selectedItemIds.length === 0) return;
141
+
142
+ alert.showWarning(
143
+ t(config.translations.deleteTitle),
144
+ t("common.delete_selected_confirm") || `Are you sure you want to delete ${selectedItemIds.length} items?`,
145
+ {
146
+ mode: AlertMode.MODAL,
147
+ actions: [
148
+ { id: 'cancel', label: t("common.cancel"), onPress: () => { } },
149
+ {
150
+ id: 'delete', label: t("common.delete"), style: 'destructive', onPress: async () => {
151
+ const success = await deleteMultipleMutation.mutateAsync(selectedItemIds);
152
+ if (success) {
153
+ setSelectedItemIds([]);
154
+ setIsSelectionMode(false);
155
+ }
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ );
161
+ }, [alert, config, deleteMultipleMutation, selectedItemIds, t]);
162
+
88
163
  // Handle viewing a creation - shows detail screen
89
164
  const handleView = useCallback((creation: Creation) => {
90
165
  setSelectedCreation(creation);
91
- }, []);
166
+ setViewerIndex(filtered.findIndex(c => c.id === creation.id));
167
+ setViewerVisible(true);
168
+ }, [filtered]);
92
169
 
93
-
94
- const styles = useStyles(tokens);
170
+ const screenTitle = headerTitle || t(config.translations.title) || 'My Creations';
171
+ const showScreenHeader = !!onBackPress;
95
172
 
96
173
  return (
97
- <View style={styles.container}>
98
- {/* Header is always shown unless we are in "System Empty" without data?
99
- User requested: "herhangi bir creations yoksa buradaki no creations gözükmeli" (if no creations, show no creations).
100
- Currently we show header always, except logic below might hide it.
101
- Actually, let's keep header always visible IF we have creations.
102
- If !creations, we pass `renderEmptyComponent`, but Grid has header support.
103
-
104
- However, to match previous request "filter gözükebilir" (filter can be visible), we'll keep header outside.
105
- BUT, if NO creations, showing filter header is weird.
106
-
107
- Let's conditonally render header: Only if we have creations OR loading.
108
- If loaded and 0 creations -> Hide header (Clean Empty State).
109
- */}
174
+ <ScreenLayout
175
+ edges={['top']}
176
+ scrollable={false}
177
+ header={
178
+ <View>
179
+ {showScreenHeader && (
180
+ <ScreenHeader
181
+ title={isSelectionMode ? `${selectedItemIds.length} Selected` : screenTitle}
182
+ onBackPress={isSelectionMode ? () => { setIsSelectionMode(false); setSelectedItemIds([]); } : onBackPress}
183
+ rightAction={isSelectionMode ? {
184
+ icon: 'trash',
185
+ onPress: handleDeleteSelected,
186
+ } : undefined}
187
+ />
188
+ )}
189
+ {!isSelectionMode && (
190
+ <View style={{ paddingHorizontal: tokens.spacing.md, paddingBottom: tokens.spacing.xs }}>
191
+ <SearchBar
192
+ placeholder={t("common.search") || "Search Prompt..."}
193
+ value={searchQuery}
194
+ onChangeText={setSearchQuery}
195
+ />
196
+ </View>
197
+ )}
198
+ </View>
199
+ }
200
+ contentContainerStyle={{ paddingHorizontal: 0 }}
201
+ >
110
202
  {(!creations || creations?.length === 0) && !isLoading ? null : (
111
203
  <GalleryHeader
112
- title={t(config.translations.title) || 'My Creations'}
204
+ title={showScreenHeader || isSelectionMode ? '' : screenTitle}
113
205
  count={filtered.length}
114
- countLabel={t(config.translations.photoCount) || 'photos'}
206
+ countLabel=''
207
+ subtitle={showCount ? t(config.translations.photoCount, { count: filtered.length }) : undefined}
115
208
  isFiltered={isFiltered}
116
209
  filterLabel={t(config.translations.filterLabel) || 'Filter'}
117
210
  onFilterPress={() => filterSheetRef.current?.present()}
118
- style={{ paddingTop: insets.top + tokens.spacing.md }}
211
+ onFavoritesPress={() => setShowOnlyFavorites(!showOnlyFavorites)}
212
+ showOnlyFavorites={showOnlyFavorites}
213
+ isFilterEnabled={config.enableFiltering ?? false}
214
+ showCount={showCount}
119
215
  />
120
216
  )}
121
217
 
@@ -128,7 +224,11 @@ export function CreationsGalleryScreen({
128
224
  onView={handleView}
129
225
  onShare={handleShare}
130
226
  onDelete={handleDelete}
131
- contentContainerStyle={{ paddingBottom: insets.bottom + tokens.spacing.xl }}
227
+ onToggleFavorite={handleToggleFavorite}
228
+ isSelectionMode={isSelectionMode}
229
+ selectedIds={selectedItemIds}
230
+ onSelect={toggleSelection}
231
+ contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
132
232
  ListEmptyComponent={
133
233
  <CreationsGalleryEmptyState
134
234
  isLoading={isLoading}
@@ -156,15 +256,11 @@ export function CreationsGalleryScreen({
156
256
  <FilterBottomSheet
157
257
  ref={filterSheetRef}
158
258
  categories={allCategories}
159
- selectedIds={selectedIds}
259
+ selectedIds={selectedCategoryIds}
160
260
  onFilterPress={(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
161
261
  onClearFilters={clearFilters}
162
262
  title={t(config.translations.filterTitle) || t("common.filter")}
163
263
  />
164
- </View>
264
+ </ScreenLayout>
165
265
  );
166
266
  }
167
-
168
- const useStyles = (tokens: any) => StyleSheet.create({
169
- container: { flex: 1, backgroundColor: tokens.colors.background },
170
- });