@umituz/react-native-ai-generation-content 1.12.11 → 1.12.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 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.12",
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,
@@ -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,6 +10,8 @@ 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;
14
16
  }
15
17
 
@@ -21,6 +23,8 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
21
23
  onFilterPress,
22
24
  filterLabel = 'Filter',
23
25
  filterIcon = 'filter-outline',
26
+ onFavoritesPress,
27
+ showOnlyFavorites = false,
24
28
  style,
25
29
  }) => {
26
30
  const tokens = useAppDesignTokens();
@@ -28,29 +32,44 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
28
32
 
29
33
  return (
30
34
  <View style={[styles.headerArea, style]}>
31
- <View>
32
- <AtomicText style={styles.title}>{title}</AtomicText>
35
+ <View style={styles.titleArea}>
36
+ {!!title && <AtomicText style={styles.title}>{title}</AtomicText>}
33
37
  <AtomicText style={styles.subtitle}>
34
38
  {count} {countLabel}
35
39
  </AtomicText>
36
40
  </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} />
41
+ <View style={styles.actions}>
42
+ {onFavoritesPress && (
43
+ <TouchableOpacity
44
+ onPress={onFavoritesPress}
45
+ style={[styles.actionButton, showOnlyFavorites && styles.actionButtonActive]}
46
+ activeOpacity={0.7}
47
+ >
48
+ <AtomicIcon
49
+ name={showOnlyFavorites ? "heart" : "heart-outline"}
50
+ size="sm"
51
+ color={showOnlyFavorites ? "error" : "secondary"}
52
+ />
53
+ </TouchableOpacity>
52
54
  )}
53
- </TouchableOpacity>
55
+ <TouchableOpacity
56
+ onPress={onFilterPress}
57
+ style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
58
+ activeOpacity={0.7}
59
+ >
60
+ <AtomicIcon
61
+ name={filterIcon}
62
+ size="sm"
63
+ color={isFiltered ? "primary" : "secondary"}
64
+ />
65
+ <AtomicText style={[styles.filterText, { color: isFiltered ? tokens.colors.primary : tokens.colors.textSecondary }]}>
66
+ {filterLabel}
67
+ </AtomicText>
68
+ {isFiltered && (
69
+ <View style={styles.badge} />
70
+ )}
71
+ </TouchableOpacity>
72
+ </View>
54
73
  </View>
55
74
  );
56
75
  };
@@ -64,6 +83,10 @@ const useStyles = (tokens: any) => StyleSheet.create({
64
83
  paddingVertical: tokens.spacing.sm,
65
84
  marginBottom: tokens.spacing.sm,
66
85
  },
86
+ titleArea: {
87
+ flex: 1,
88
+ marginRight: tokens.spacing.md,
89
+ },
67
90
  title: {
68
91
  fontSize: 20,
69
92
  fontWeight: "700",
@@ -75,6 +98,25 @@ const useStyles = (tokens: any) => StyleSheet.create({
75
98
  color: tokens.colors.textSecondary,
76
99
  opacity: 0.6
77
100
  },
101
+ actions: {
102
+ flexDirection: 'row',
103
+ alignItems: 'center',
104
+ gap: tokens.spacing.sm,
105
+ },
106
+ actionButton: {
107
+ width: 40,
108
+ height: 40,
109
+ borderRadius: 20,
110
+ backgroundColor: tokens.colors.surfaceVariant,
111
+ justifyContent: 'center',
112
+ alignItems: 'center',
113
+ borderWidth: 1,
114
+ borderColor: 'transparent',
115
+ },
116
+ actionButtonActive: {
117
+ backgroundColor: tokens.colors.primary + "15",
118
+ borderColor: tokens.colors.primary + "30",
119
+ },
78
120
  filterButton: {
79
121
  flexDirection: 'row',
80
122
  alignItems: 'center',
@@ -85,6 +127,7 @@ const useStyles = (tokens: any) => StyleSheet.create({
85
127
  backgroundColor: tokens.colors.surfaceVariant,
86
128
  borderWidth: 1,
87
129
  borderColor: 'transparent',
130
+ height: 40,
88
131
  },
89
132
  filterButtonActive: {
90
133
  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,19 +1,32 @@
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;
@@ -24,6 +37,8 @@ interface CreationsGalleryScreenProps {
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;
27
42
  }
28
43
 
29
44
  export function CreationsGalleryScreen({
@@ -35,9 +50,10 @@ export function CreationsGalleryScreen({
35
50
  onImageEdit,
36
51
  onEmptyAction,
37
52
  emptyActionLabel,
53
+ onBackPress,
54
+ headerTitle,
38
55
  }: CreationsGalleryScreenProps) {
39
56
  const tokens = useAppDesignTokens();
40
- const insets = useSafeAreaInsets();
41
57
  const { share } = useSharing();
42
58
  const alert = useAlert();
43
59
 
@@ -48,8 +64,27 @@ export function CreationsGalleryScreen({
48
64
 
49
65
  const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
50
66
  const creations = creationsData as Creation[] | undefined;
67
+
51
68
  const deleteMutation = useDeleteCreation({ userId, repository });
52
- const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
69
+ const toggleFavoriteMutation = useToggleFavorite({ userId, repository });
70
+ const deleteMultipleMutation = useDeleteMultipleCreations({ userId, repository });
71
+
72
+ const {
73
+ filtered,
74
+ selectedIds: selectedCategoryIds,
75
+ searchQuery,
76
+ setSearchQuery,
77
+ showOnlyFavorites,
78
+ setShowOnlyFavorites,
79
+ toggleFilter,
80
+ clearFilters,
81
+ isFiltered
82
+ } = useCreationsFilter({ creations });
83
+
84
+ const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
85
+ const [isSelectionMode, setIsSelectionMode] = useState(false);
86
+
87
+
53
88
 
54
89
  // Refetch creations when screen comes into focus
55
90
  useFocusEffect(
@@ -85,37 +120,93 @@ export function CreationsGalleryScreen({
85
120
  );
86
121
  }, [alert, config, deleteMutation, t]);
87
122
 
123
+ const handleToggleFavorite = useCallback((creation: Creation) => {
124
+ toggleFavoriteMutation.mutate(creation.id);
125
+ }, [toggleFavoriteMutation]);
126
+
127
+ const toggleSelection = useCallback((creation: Creation) => {
128
+ setSelectedItemIds(prev => {
129
+ const isSelected = prev.includes(creation.id);
130
+ const next = isSelected ? prev.filter(id => id !== creation.id) : [...prev, creation.id];
131
+ if (next.length === 0) setIsSelectionMode(false);
132
+ else setIsSelectionMode(true);
133
+ return next;
134
+ });
135
+ }, []);
136
+
137
+ const handleDeleteSelected = useCallback(() => {
138
+ if (selectedItemIds.length === 0) return;
139
+
140
+ alert.showWarning(
141
+ t(config.translations.deleteTitle),
142
+ t("common.delete_selected_confirm") || `Are you sure you want to delete ${selectedItemIds.length} items?`,
143
+ {
144
+ mode: AlertMode.MODAL,
145
+ actions: [
146
+ { id: 'cancel', label: t("common.cancel"), onPress: () => { } },
147
+ {
148
+ id: 'delete', label: t("common.delete"), style: 'destructive', onPress: async () => {
149
+ const success = await deleteMultipleMutation.mutateAsync(selectedItemIds);
150
+ if (success) {
151
+ setSelectedItemIds([]);
152
+ setIsSelectionMode(false);
153
+ }
154
+ }
155
+ }
156
+ ]
157
+ }
158
+ );
159
+ }, [alert, config, deleteMultipleMutation, selectedItemIds, t]);
160
+
88
161
  // Handle viewing a creation - shows detail screen
89
162
  const handleView = useCallback((creation: Creation) => {
90
163
  setSelectedCreation(creation);
91
- }, []);
164
+ setViewerIndex(filtered.findIndex(c => c.id === creation.id));
165
+ setViewerVisible(true);
166
+ }, [filtered]);
92
167
 
93
-
94
- const styles = useStyles(tokens);
168
+ const screenTitle = headerTitle || t(config.translations.title) || 'My Creations';
169
+ const showScreenHeader = !!onBackPress;
95
170
 
96
171
  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
- */}
172
+ <ScreenLayout
173
+ edges={['top']}
174
+ scrollable={false}
175
+ header={
176
+ <View>
177
+ {showScreenHeader && (
178
+ <ScreenHeader
179
+ title={isSelectionMode ? `${selectedItemIds.length} Selected` : screenTitle}
180
+ onBackPress={isSelectionMode ? () => { setIsSelectionMode(false); setSelectedItemIds([]); } : onBackPress}
181
+ rightAction={isSelectionMode ? {
182
+ icon: 'trash',
183
+ onPress: handleDeleteSelected,
184
+ } : undefined}
185
+ />
186
+ )}
187
+ {!isSelectionMode && (
188
+ <View style={{ paddingHorizontal: tokens.spacing.md, paddingBottom: tokens.spacing.xs }}>
189
+ <SearchBar
190
+ placeholder={t("common.search") || "Search Prompt..."}
191
+ value={searchQuery}
192
+ onChangeText={setSearchQuery}
193
+ />
194
+ </View>
195
+ )}
196
+ </View>
197
+ }
198
+ contentContainerStyle={{ paddingHorizontal: 0 }}
199
+ >
110
200
  {(!creations || creations?.length === 0) && !isLoading ? null : (
111
201
  <GalleryHeader
112
- title={t(config.translations.title) || 'My Creations'}
202
+ title={showScreenHeader || isSelectionMode ? '' : screenTitle}
113
203
  count={filtered.length}
114
204
  countLabel={t(config.translations.photoCount) || 'photos'}
115
205
  isFiltered={isFiltered}
116
206
  filterLabel={t(config.translations.filterLabel) || 'Filter'}
117
207
  onFilterPress={() => filterSheetRef.current?.present()}
118
- style={{ paddingTop: insets.top + tokens.spacing.md }}
208
+ onFavoritesPress={() => setShowOnlyFavorites(!showOnlyFavorites)}
209
+ showOnlyFavorites={showOnlyFavorites}
119
210
  />
120
211
  )}
121
212
 
@@ -128,7 +219,11 @@ export function CreationsGalleryScreen({
128
219
  onView={handleView}
129
220
  onShare={handleShare}
130
221
  onDelete={handleDelete}
131
- contentContainerStyle={{ paddingBottom: insets.bottom + tokens.spacing.xl }}
222
+ onToggleFavorite={handleToggleFavorite}
223
+ isSelectionMode={isSelectionMode}
224
+ selectedIds={selectedItemIds}
225
+ onSelect={toggleSelection}
226
+ contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
132
227
  ListEmptyComponent={
133
228
  <CreationsGalleryEmptyState
134
229
  isLoading={isLoading}
@@ -156,15 +251,11 @@ export function CreationsGalleryScreen({
156
251
  <FilterBottomSheet
157
252
  ref={filterSheetRef}
158
253
  categories={allCategories}
159
- selectedIds={selectedIds}
254
+ selectedIds={selectedCategoryIds}
160
255
  onFilterPress={(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
161
256
  onClearFilters={clearFilters}
162
257
  title={t(config.translations.filterTitle) || t("common.filter")}
163
258
  />
164
- </View>
259
+ </ScreenLayout>
165
260
  );
166
261
  }
167
-
168
- const useStyles = (tokens: any) => StyleSheet.create({
169
- container: { flex: 1, backgroundColor: tokens.colors.background },
170
- });