@umituz/react-native-ai-creations 1.2.2 → 1.2.4

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-creations",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "AI-generated creations gallery with filtering, sharing, and management for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -34,18 +34,24 @@
34
34
  "@umituz/react-native-image": "latest",
35
35
  "@umituz/react-native-sharing": "latest",
36
36
  "firebase": ">=11.0.0",
37
- "react": ">=18.2.0",
38
- "react-native": ">=0.74.0",
39
- "react-native-safe-area-context": ">=4.0.0"
37
+ "react": ">=19.1.0",
38
+ "react-native": ">=0.81.5",
39
+ "react-native-safe-area-context": ">=5.0.0",
40
+ "@umituz/react-native-filter": "^1.4.2",
41
+ "@umituz/react-native-bottom-sheet": "latest",
42
+ "@umituz/react-native-design-system-atoms": "latest"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@tanstack/react-query": "^5.62.16",
43
46
  "@types/react": "^19.0.0",
44
47
  "@umituz/react-native-image": "^1.1.1",
45
48
  "firebase": "^11.0.0",
46
- "react": "^19.0.0",
47
- "react-native": "^0.76.0",
48
- "react-native-safe-area-context": "^4.14.0",
49
+ "react": "19.1.0",
50
+ "react-native": "0.81.5",
51
+ "react-native-safe-area-context": "^5.6.0",
52
+ "@umituz/react-native-filter": "^1.4.2",
53
+ "@umituz/react-native-bottom-sheet": "latest",
54
+ "@umituz/react-native-design-system-atoms": "latest",
49
55
  "typescript": "^5.3.3"
50
56
  },
51
57
  "publishConfig": {
@@ -39,9 +39,12 @@ export type PathBuilder = (userId: string) => string[];
39
39
  */
40
40
  export type DocumentMapper = (id: string, data: CreationDocument) => Creation;
41
41
 
42
+ import type { FilterCategory } from "@umituz/react-native-filter";
43
+
42
44
  export interface CreationsConfig {
43
45
  readonly collectionName: string;
44
46
  readonly types: readonly CreationType[];
47
+ readonly filterCategories?: readonly FilterCategory[];
45
48
  readonly translations: CreationsTranslations;
46
49
  readonly maxThumbnails?: number;
47
50
  readonly gridColumns?: number;
@@ -14,6 +14,7 @@ interface FilterChipsProps {
14
14
  readonly selectedType: string;
15
15
  readonly allLabel: string;
16
16
  readonly onSelect: (type: string) => void;
17
+ readonly style?: any;
17
18
  }
18
19
 
19
20
  export function FilterChips({
@@ -22,6 +23,7 @@ export function FilterChips({
22
23
  selectedType,
23
24
  allLabel,
24
25
  onSelect,
26
+ style,
25
27
  }: FilterChipsProps) {
26
28
  const tokens = useAppDesignTokens();
27
29
 
@@ -59,7 +61,7 @@ export function FilterChips({
59
61
  const visibleTypes = types.filter((t) => availableTypes.includes(t.id));
60
62
 
61
63
  return (
62
- <View style={styles.container}>
64
+ <View style={[styles.container, style]}>
63
65
  <ScrollView
64
66
  horizontal
65
67
  showsHorizontalScrollIndicator={false}
@@ -12,35 +12,42 @@ interface UseCreationsFilterProps {
12
12
  readonly creations: Creation[] | undefined;
13
13
  }
14
14
 
15
- export function useCreationsFilter({ creations }: UseCreationsFilterProps) {
16
- const [selectedType, setSelectedType] = useState<string>(ALL_FILTER);
15
+ import { FilterUtils } from "@umituz/react-native-filter";
17
16
 
18
- const availableTypes = useMemo(() => {
19
- if (!creations) return [];
20
- const types = new Set(creations.map((c) => c.type));
21
- return Array.from(types);
22
- }, [creations]);
17
+ interface UseCreationsFilterProps {
18
+ readonly creations: Creation[] | undefined;
19
+ readonly defaultFilterId?: string;
20
+ }
21
+
22
+ export function useCreationsFilter({
23
+ creations,
24
+ defaultFilterId = "all"
25
+ }: UseCreationsFilterProps) {
26
+ const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
23
27
 
24
28
  const filtered = useMemo(() => {
25
29
  if (!creations) return [];
26
- if (selectedType === ALL_FILTER) return creations;
27
- return creations.filter((c) => c.type === selectedType);
28
- }, [creations, selectedType]);
30
+ if (selectedIds.includes(defaultFilterId)) return creations;
31
+
32
+ return creations.filter((c) =>
33
+ selectedIds.includes(c.type) ||
34
+ selectedIds.some(id => (c as any).metadata?.tags?.includes(id))
35
+ );
36
+ }, [creations, selectedIds, defaultFilterId]);
29
37
 
30
- const selectType = useCallback((type: string) => {
31
- setSelectedType(type);
32
- }, []);
38
+ const toggleFilter = useCallback((filterId: string, multiSelect: boolean = false) => {
39
+ setSelectedIds(prev => FilterUtils.toggleFilter(prev, filterId, multiSelect, defaultFilterId));
40
+ }, [defaultFilterId]);
33
41
 
34
- const resetFilter = useCallback(() => {
35
- setSelectedType(ALL_FILTER);
36
- }, []);
42
+ const clearFilters = useCallback(() => {
43
+ setSelectedIds([defaultFilterId]);
44
+ }, [defaultFilterId]);
37
45
 
38
46
  return {
39
47
  filtered,
40
- selectedType,
41
- availableTypes,
42
- selectType,
43
- resetFilter,
44
- isFiltered: selectedType !== ALL_FILTER,
48
+ selectedIds,
49
+ toggleFilter,
50
+ clearFilters,
51
+ isFiltered: !selectedIds.includes(defaultFilterId),
45
52
  };
46
53
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import React, { useMemo, useCallback, useState } from "react";
7
- import { View, FlatList, StyleSheet, RefreshControl, Alert } from "react-native";
7
+ import { View, FlatList, StyleSheet, RefreshControl, Alert, TouchableOpacity } from "react-native";
8
8
  import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
9
  import { useSharing } from "@umituz/react-native-sharing";
10
10
  import { ImageGallery } from "@umituz/react-native-image";
@@ -16,8 +16,10 @@ import { useCreations } from "../hooks/useCreations";
16
16
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
17
17
  import { useCreationsFilter } from "../hooks/useCreationsFilter";
18
18
  import { CreationCard } from "../components/CreationCard";
19
- import { FilterChips } from "../components/FilterChips";
20
19
  import { EmptyState } from "../components/EmptyState";
20
+ import { FilterBottomSheet, type FilterCategory } from "@umituz/react-native-filter";
21
+ import { BottomSheetModalRef } from "@umituz/react-native-bottom-sheet";
22
+ import { AtomicIcon, AtomicText, AtomicButton } from "@umituz/react-native-design-system-atoms";
21
23
 
22
24
  interface CreationsGalleryScreenProps {
23
25
  readonly userId: string | null;
@@ -41,6 +43,7 @@ export function CreationsGalleryScreen({
41
43
  const { share } = useSharing();
42
44
  const [viewerVisible, setViewerVisible] = useState(false);
43
45
  const [viewerIndex, setViewerIndex] = useState(0);
46
+ const filterSheetRef = React.useRef<BottomSheetModalRef>(null);
44
47
 
45
48
  const { data: creations, isLoading, refetch } = useCreations({
46
49
  userId,
@@ -48,9 +51,33 @@ export function CreationsGalleryScreen({
48
51
  });
49
52
 
50
53
  const deleteMutation = useDeleteCreation({ userId, repository });
51
- const { filtered, selectedType, availableTypes, selectType } =
54
+ const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } =
52
55
  useCreationsFilter({ creations });
53
56
 
57
+ const allCategories = useMemo(() => {
58
+ const categories: FilterCategory[] = [];
59
+
60
+ // Add dynamic types category if types exist
61
+ if (config.types.length > 0) {
62
+ categories.push({
63
+ id: 'type', // This ID matches logic in useFilter? No, filter logic is flat ID based. 'type' is category ID.
64
+ title: t('creations.category') || 'Category',
65
+ multiSelect: false,
66
+ options: config.types.map(t => ({
67
+ id: t.id,
68
+ label: t.labelKey, // In a real app we'd translate this, but for now passing key or mocked
69
+ icon: 'Image', // Default icon since types have emojis which might not work with AtomicIcon
70
+ }))
71
+ });
72
+ }
73
+
74
+ if (config.filterCategories) {
75
+ categories.push(...config.filterCategories);
76
+ }
77
+
78
+ return categories;
79
+ }, [config.types, config.filterCategories, t]);
80
+
54
81
  const handleView = useCallback(
55
82
  (creation: Creation) => {
56
83
  const index = filtered.findIndex((c) => c.id === creation.id);
@@ -111,8 +138,40 @@ export function CreationsGalleryScreen({
111
138
  flex: 1,
112
139
  backgroundColor: tokens.colors.backgroundPrimary,
113
140
  },
141
+ headerArea: {
142
+ flexDirection: "row",
143
+ alignItems: "center",
144
+ justifyContent: 'space-between',
145
+ paddingHorizontal: tokens.spacing.md,
146
+ paddingVertical: tokens.spacing.sm,
147
+ marginBottom: tokens.spacing.sm,
148
+ },
149
+ filterButton: {
150
+ flexDirection: 'row',
151
+ alignItems: 'center',
152
+ gap: tokens.spacing.xs,
153
+ paddingVertical: tokens.spacing.xs,
154
+ paddingHorizontal: tokens.spacing.md,
155
+ borderRadius: (tokens as any).borders?.radius?.full || 999,
156
+ backgroundColor: tokens.colors.surfaceVariant,
157
+ borderWidth: 1,
158
+ borderColor: 'transparent',
159
+ },
160
+ filterButtonActive: {
161
+ backgroundColor: tokens.colors.primary + "15",
162
+ borderColor: tokens.colors.primary + "30",
163
+ },
164
+ badge: {
165
+ width: 8,
166
+ height: 8,
167
+ borderRadius: 4,
168
+ backgroundColor: tokens.colors.primary,
169
+ position: 'absolute',
170
+ top: 6,
171
+ right: 6,
172
+ },
114
173
  list: {
115
- padding: tokens.spacing.md,
174
+ paddingHorizontal: tokens.spacing.md,
116
175
  paddingBottom: insets.bottom + tokens.spacing.xl,
117
176
  gap: tokens.spacing.md,
118
177
  },
@@ -151,15 +210,22 @@ export function CreationsGalleryScreen({
151
210
 
152
211
  return (
153
212
  <View style={styles.container}>
154
- {config.types.length > 0 && availableTypes.length > 1 && (
155
- <FilterChips
156
- types={config.types}
157
- availableTypes={availableTypes}
158
- selectedType={selectedType}
159
- allLabel={t(config.translations.filterAll)}
160
- onSelect={selectType}
161
- />
162
- )}
213
+ <View style={styles.headerArea}>
214
+ <View>
215
+ <AtomicText type="headlineSmall">{t(config.translations.title) || 'My Creations'}</AtomicText>
216
+ <AtomicText type="bodySmall" style={{ opacity: 0.6 }}>{filtered.length} {t(config.translations.photoCount) || 'photos'}</AtomicText>
217
+ </View>
218
+ <TouchableOpacity
219
+ onPress={() => filterSheetRef.current?.present()}
220
+ style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
221
+ >
222
+ <AtomicIcon name="ListFilter" size="sm" color={isFiltered ? "primary" : "secondary"} />
223
+ <AtomicText type="labelMedium" color={isFiltered ? "primary" : "secondary"}>Filter</AtomicText>
224
+ {isFiltered && (
225
+ <View style={styles.badge} />
226
+ )}
227
+ </TouchableOpacity>
228
+ </View>
163
229
  <FlatList
164
230
  data={filtered}
165
231
  renderItem={renderItem}
@@ -182,6 +248,18 @@ export function CreationsGalleryScreen({
182
248
  enableEditing={enableEditing}
183
249
  onImageChange={handleImageChange}
184
250
  />
251
+
252
+ <FilterBottomSheet
253
+ ref={filterSheetRef}
254
+ categories={allCategories as FilterCategory[]}
255
+ selectedIds={selectedIds}
256
+ onFilterPress={(id, catId) => {
257
+ const category = allCategories.find(c => c.id === catId);
258
+ toggleFilter(id, category?.multiSelect);
259
+ }}
260
+ onClearFilters={clearFilters}
261
+ title={t("common.filter")}
262
+ />
185
263
  </View>
186
264
  );
187
265
  }