@umituz/react-native-ai-creations 1.3.2 → 1.3.5
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 +2 -2
- package/src/presentation/components/CreationDetail/DetailImage.tsx +0 -5
- package/src/presentation/components/FilterBottomSheet.tsx +157 -0
- package/src/presentation/components/index.ts +1 -0
- package/src/presentation/hooks/useCreationsFilter.ts +26 -5
- package/src/presentation/screens/CreationsGalleryScreen.tsx +12 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-creations",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
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",
|
|
@@ -68,4 +68,4 @@
|
|
|
68
68
|
"README.md",
|
|
69
69
|
"LICENSE"
|
|
70
70
|
]
|
|
71
|
-
}
|
|
71
|
+
}
|
|
@@ -33,11 +33,6 @@ const useStyles = (tokens: any) => StyleSheet.create({
|
|
|
33
33
|
borderRadius: 24,
|
|
34
34
|
overflow: 'hidden',
|
|
35
35
|
backgroundColor: tokens.colors.surface,
|
|
36
|
-
shadowColor: "#000",
|
|
37
|
-
shadowOffset: { width: 0, height: 8 },
|
|
38
|
-
shadowOpacity: 0.2,
|
|
39
|
-
shadowRadius: 16,
|
|
40
|
-
elevation: 8,
|
|
41
36
|
},
|
|
42
37
|
image: {
|
|
43
38
|
width: '100%',
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
|
3
|
+
import { BottomSheetModal, BottomSheetModalRef } from '@umituz/react-native-bottom-sheet';
|
|
4
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
5
|
+
|
|
6
|
+
export interface FilterOption {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FilterCategory {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
multiSelect?: boolean;
|
|
16
|
+
options: FilterOption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FilterBottomSheetProps {
|
|
20
|
+
categories: FilterCategory[];
|
|
21
|
+
selectedIds: string[];
|
|
22
|
+
onFilterPress: (id: string, categoryId: string) => void;
|
|
23
|
+
onClearFilters: () => void;
|
|
24
|
+
title: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const FilterBottomSheet = forwardRef<BottomSheetModalRef, FilterBottomSheetProps>((props, ref) => {
|
|
28
|
+
const { categories, selectedIds, onFilterPress, onClearFilters, title } = props;
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
const styles = useStyles(tokens);
|
|
31
|
+
|
|
32
|
+
const snapPoints = useMemo(() => ['50%', '75%'], []);
|
|
33
|
+
|
|
34
|
+
const renderOption = useCallback((option: FilterOption, category: FilterCategory) => {
|
|
35
|
+
const isSelected = selectedIds.includes(option.id);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<TouchableOpacity
|
|
39
|
+
key={option.id}
|
|
40
|
+
style={[styles.option, isSelected && styles.optionSelected]}
|
|
41
|
+
onPress={() => onFilterPress(option.id, category.id)}
|
|
42
|
+
>
|
|
43
|
+
<View style={styles.optionContent}>
|
|
44
|
+
{option.icon && (
|
|
45
|
+
<View style={styles.optionIcon}>
|
|
46
|
+
<AtomicIcon
|
|
47
|
+
name={option.icon as any}
|
|
48
|
+
size="sm"
|
|
49
|
+
color={isSelected ? "primary" : "text"}
|
|
50
|
+
/>
|
|
51
|
+
</View>
|
|
52
|
+
)}
|
|
53
|
+
<AtomicText
|
|
54
|
+
style={[styles.optionLabel, isSelected && styles.optionLabelSelected]}
|
|
55
|
+
>
|
|
56
|
+
{option.label}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</View>
|
|
59
|
+
{isSelected && (
|
|
60
|
+
<AtomicIcon name="check" size="sm" color="primary" />
|
|
61
|
+
)}
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
);
|
|
64
|
+
}, [onFilterPress, selectedIds, styles]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<BottomSheetModal
|
|
68
|
+
ref={ref}
|
|
69
|
+
snapPoints={snapPoints}
|
|
70
|
+
>
|
|
71
|
+
<View style={styles.header}>
|
|
72
|
+
<AtomicText style={styles.headerTitle}>{title}</AtomicText>
|
|
73
|
+
<TouchableOpacity onPress={onClearFilters}>
|
|
74
|
+
<AtomicText style={styles.clearButton}>Clear</AtomicText>
|
|
75
|
+
</TouchableOpacity>
|
|
76
|
+
</View>
|
|
77
|
+
|
|
78
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
79
|
+
{categories.map(category => (
|
|
80
|
+
<View key={category.id} style={styles.categoryContainer}>
|
|
81
|
+
<AtomicText style={styles.categoryTitle}>{category.title}</AtomicText>
|
|
82
|
+
<View style={styles.optionsContainer}>
|
|
83
|
+
{category.options.map(option => renderOption(option, category))}
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
))}
|
|
87
|
+
</ScrollView>
|
|
88
|
+
</BottomSheetModal>
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const useStyles = (tokens: any) => StyleSheet.create({
|
|
93
|
+
content: {
|
|
94
|
+
padding: tokens.spacing.md,
|
|
95
|
+
paddingBottom: tokens.spacing.xl,
|
|
96
|
+
},
|
|
97
|
+
header: {
|
|
98
|
+
flexDirection: 'row',
|
|
99
|
+
justifyContent: 'space-between',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
paddingHorizontal: tokens.spacing.md,
|
|
102
|
+
paddingBottom: tokens.spacing.sm,
|
|
103
|
+
borderBottomWidth: 1,
|
|
104
|
+
borderBottomColor: tokens.colors.outline,
|
|
105
|
+
},
|
|
106
|
+
headerTitle: {
|
|
107
|
+
fontSize: 20,
|
|
108
|
+
fontWeight: '700',
|
|
109
|
+
color: tokens.colors.textPrimary,
|
|
110
|
+
},
|
|
111
|
+
clearButton: {
|
|
112
|
+
color: tokens.colors.primary,
|
|
113
|
+
fontSize: 14,
|
|
114
|
+
fontWeight: '600',
|
|
115
|
+
},
|
|
116
|
+
categoryContainer: {
|
|
117
|
+
marginTop: tokens.spacing.md,
|
|
118
|
+
},
|
|
119
|
+
categoryTitle: {
|
|
120
|
+
marginBottom: tokens.spacing.xs,
|
|
121
|
+
color: tokens.colors.textSecondary,
|
|
122
|
+
fontSize: 16,
|
|
123
|
+
fontWeight: '600',
|
|
124
|
+
},
|
|
125
|
+
optionsContainer: {
|
|
126
|
+
backgroundColor: tokens.colors.background,
|
|
127
|
+
borderRadius: tokens.borderRadius.md,
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
},
|
|
130
|
+
option: {
|
|
131
|
+
flexDirection: 'row',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
justifyContent: 'space-between',
|
|
134
|
+
padding: tokens.spacing.md,
|
|
135
|
+
backgroundColor: tokens.colors.background,
|
|
136
|
+
borderBottomWidth: 1,
|
|
137
|
+
borderBottomColor: tokens.colors.surface,
|
|
138
|
+
},
|
|
139
|
+
optionSelected: {
|
|
140
|
+
backgroundColor: tokens.colors.surface,
|
|
141
|
+
},
|
|
142
|
+
optionContent: {
|
|
143
|
+
flexDirection: 'row',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
},
|
|
146
|
+
optionIcon: {
|
|
147
|
+
marginRight: tokens.spacing.sm,
|
|
148
|
+
},
|
|
149
|
+
optionLabel: {
|
|
150
|
+
color: tokens.colors.text,
|
|
151
|
+
fontSize: 14,
|
|
152
|
+
},
|
|
153
|
+
optionLabelSelected: {
|
|
154
|
+
color: tokens.colors.primary,
|
|
155
|
+
fontWeight: 'bold',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
@@ -9,6 +9,7 @@ export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
|
9
9
|
export { CreationCard } from "./CreationCard";
|
|
10
10
|
export { CreationThumbnail } from "./CreationThumbnail";
|
|
11
11
|
export { CreationsGrid } from "./CreationsGrid";
|
|
12
|
+
export { FilterBottomSheet, type FilterCategory, type FilterOption } from "./FilterBottomSheet";
|
|
12
13
|
|
|
13
14
|
// Detail Components
|
|
14
15
|
export { DetailHeader } from "./CreationDetail/DetailHeader";
|
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
import { useState, useMemo, useCallback } from "react";
|
|
7
7
|
import type { Creation } from "../../domain/entities/Creation";
|
|
8
8
|
|
|
9
|
-
const ALL_FILTER = "all";
|
|
10
|
-
|
|
11
|
-
import { FilterUtils } from "@umituz/react-native-bottom-sheet";
|
|
12
|
-
|
|
13
9
|
interface UseCreationsFilterProps {
|
|
14
10
|
readonly creations: Creation[] | undefined;
|
|
15
11
|
readonly defaultFilterId?: string;
|
|
@@ -32,7 +28,32 @@ export function useCreationsFilter({
|
|
|
32
28
|
}, [creations, selectedIds, defaultFilterId]);
|
|
33
29
|
|
|
34
30
|
const toggleFilter = useCallback((filterId: string, multiSelect: boolean = false) => {
|
|
35
|
-
setSelectedIds(prev =>
|
|
31
|
+
setSelectedIds(prev => {
|
|
32
|
+
// If selecting 'all', clear everything else
|
|
33
|
+
if (filterId === defaultFilterId) return [defaultFilterId];
|
|
34
|
+
|
|
35
|
+
let newIds: string[];
|
|
36
|
+
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
|
+
if (prev.includes(filterId) && prev.length === 1) return prev;
|
|
42
|
+
newIds = [filterId];
|
|
43
|
+
} else {
|
|
44
|
+
// Multi select
|
|
45
|
+
if (prev.includes(filterId)) {
|
|
46
|
+
newIds = prev.filter(id => id !== filterId);
|
|
47
|
+
} else {
|
|
48
|
+
// Remove 'all' if present
|
|
49
|
+
newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If nothing selected, revert to 'all'
|
|
54
|
+
if (newIds.length === 0) return [defaultFilterId];
|
|
55
|
+
return newIds;
|
|
56
|
+
});
|
|
36
57
|
}, [defaultFilterId]);
|
|
37
58
|
|
|
38
59
|
const clearFilters = useCallback(() => {
|
|
@@ -9,9 +9,8 @@ import { useCreations } from "../hooks/useCreations";
|
|
|
9
9
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
10
10
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
11
11
|
import { useAlert } from "@umituz/react-native-alert";
|
|
12
|
-
import { FilterBottomSheet, type FilterCategory } from "@umituz/react-native-bottom-sheet";
|
|
13
12
|
import { BottomSheetModalRef } from "@umituz/react-native-bottom-sheet";
|
|
14
|
-
import { GalleryHeader, EmptyState, CreationsGrid } from "../components";
|
|
13
|
+
import { GalleryHeader, EmptyState, CreationsGrid, FilterBottomSheet, type FilterCategory } from "../components";
|
|
15
14
|
import type { Creation } from "../../domain/entities/Creation";
|
|
16
15
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
17
16
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
@@ -59,30 +58,27 @@ export function CreationsGalleryScreen({
|
|
|
59
58
|
}, [refetch])
|
|
60
59
|
);
|
|
61
60
|
|
|
62
|
-
// Translate types for Grid display
|
|
61
|
+
// Translate types for Grid display & Filter
|
|
63
62
|
const translatedTypes = useMemo(() => {
|
|
64
63
|
return config.types.map(type => ({
|
|
65
64
|
...type,
|
|
66
|
-
labelKey: t(type.labelKey)
|
|
65
|
+
labelKey: t(type.labelKey)
|
|
67
66
|
}));
|
|
68
67
|
}, [config.types, t]);
|
|
69
68
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
69
|
+
const allCategories = useMemo(() => {
|
|
70
|
+
const categories: FilterCategory[] = [];
|
|
72
71
|
if (config.types.length > 0) {
|
|
73
|
-
|
|
72
|
+
categories.push({
|
|
74
73
|
id: 'type',
|
|
75
74
|
title: t(config.translations.filterTitle),
|
|
76
75
|
multiSelect: false,
|
|
77
76
|
options: config.types.map(type => ({ id: type.id, label: t(type.labelKey), icon: type.icon || 'image' }))
|
|
78
77
|
});
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// For now, allCategories is just the 'categories' derived from config.types
|
|
84
|
-
// In the future, other filter categories could be added here.
|
|
85
|
-
const allCategories = useMemo(() => categories, [categories]);
|
|
79
|
+
if (config.filterCategories) categories.push(...config.filterCategories);
|
|
80
|
+
return categories;
|
|
81
|
+
}, [config.types, config.filterCategories, t, config.translations.filterTitle]);
|
|
86
82
|
|
|
87
83
|
const handleShare = useCallback(async (creation: Creation) => {
|
|
88
84
|
share(creation.uri, { dialogTitle: t("common.share") });
|
|
@@ -105,8 +101,6 @@ export function CreationsGalleryScreen({
|
|
|
105
101
|
});
|
|
106
102
|
}, [alert, config, deleteMutation, t]);
|
|
107
103
|
|
|
108
|
-
const styles = useStyles(tokens);
|
|
109
|
-
|
|
110
104
|
if (selectedCreation) {
|
|
111
105
|
return (
|
|
112
106
|
<CreationDetailScreen
|
|
@@ -119,6 +113,8 @@ export function CreationsGalleryScreen({
|
|
|
119
113
|
);
|
|
120
114
|
}
|
|
121
115
|
|
|
116
|
+
const styles = useStyles(tokens);
|
|
117
|
+
|
|
122
118
|
if (!isLoading && (!creations || creations.length === 0)) {
|
|
123
119
|
return (
|
|
124
120
|
<View style={styles.container}>
|
|
@@ -178,19 +174,7 @@ export function CreationsGalleryScreen({
|
|
|
178
174
|
);
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
/>
|
|
183
|
-
< FilterBottomSheet
|
|
184
|
-
ref = { filterSheetRef }
|
|
185
|
-
categories = { allCategories }
|
|
186
|
-
selectedIds = { selectedIds }
|
|
187
|
-
onFilterPress = {(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
|
|
188
|
-
onClearFilters = { clearFilters }
|
|
189
|
-
title = { t(config.translations.filterTitle) || t("common.filter")}
|
|
190
|
-
/>
|
|
191
|
-
</View >
|
|
192
|
-
);
|
|
193
|
-
}
|
|
177
|
+
|
|
194
178
|
|
|
195
179
|
const useStyles = (tokens: any) => StyleSheet.create({
|
|
196
180
|
container: { flex: 1, backgroundColor: tokens.colors.background },
|