@umituz/react-native-ai-creations 1.2.10 → 1.2.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-creations",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
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",
@@ -28,10 +28,11 @@
28
28
  "url": "https://github.com/umituz/react-native-ai-creations"
29
29
  },
30
30
  "dependencies": {
31
+ "@umituz/react-native-alert": "latest",
32
+ "@umituz/react-native-bottom-sheet": "latest",
31
33
  "@umituz/react-native-filter": "latest",
32
34
  "@umituz/react-native-image": "latest",
33
- "@umituz/react-native-bottom-sheet": "latest",
34
- "@umituz/react-native-alert": "latest"
35
+ "expo-linear-gradient": "^15.0.8"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@tanstack/react-query": ">=5.0.0",
@@ -47,8 +48,8 @@
47
48
  "@tanstack/react-query": "^5.62.16",
48
49
  "@types/react": "^19.0.0",
49
50
  "@umituz/react-native-design-system": "latest",
50
- "@umituz/react-native-sharing": "latest",
51
51
  "@umituz/react-native-firestore": "latest",
52
+ "@umituz/react-native-sharing": "latest",
52
53
  "firebase": "^11.0.0",
53
54
  "react": "19.1.0",
54
55
  "react-native": "0.81.5",
@@ -0,0 +1,74 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
4
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
5
+
6
+ interface DetailActionsProps {
7
+ readonly onShare: () => void;
8
+ readonly onDelete: () => void;
9
+ readonly shareLabel: string;
10
+ readonly deleteLabel: string;
11
+ }
12
+
13
+ export const DetailActions: React.FC<DetailActionsProps> = ({
14
+ onShare,
15
+ onDelete,
16
+ shareLabel,
17
+ deleteLabel
18
+ }) => {
19
+ const tokens = useAppDesignTokens();
20
+ const styles = useStyles(tokens);
21
+
22
+ return (
23
+ <View style={styles.container}>
24
+ <TouchableOpacity
25
+ style={[styles.button, styles.shareButton]}
26
+ onPress={onShare}
27
+ >
28
+ <AtomicIcon name="share-social" size="sm" color="#fff" />
29
+ <AtomicText style={styles.buttonText}>{shareLabel}</AtomicText>
30
+ </TouchableOpacity>
31
+
32
+ <TouchableOpacity
33
+ style={[styles.button, styles.deleteButton]}
34
+ onPress={onDelete}
35
+ >
36
+ <AtomicIcon name="trash" size="sm" color={tokens.colors.error} />
37
+ <AtomicText style={[styles.buttonText, { color: tokens.colors.error }]}>
38
+ {deleteLabel}
39
+ </AtomicText>
40
+ </TouchableOpacity>
41
+ </View>
42
+ );
43
+ };
44
+
45
+ const useStyles = (tokens: any) => StyleSheet.create({
46
+ container: {
47
+ flexDirection: 'row',
48
+ justifyContent: 'center',
49
+ gap: tokens.spacing.md,
50
+ paddingHorizontal: tokens.spacing.lg,
51
+ marginBottom: tokens.spacing.xxl,
52
+ },
53
+ button: {
54
+ flexDirection: 'row',
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ paddingVertical: 12,
58
+ paddingHorizontal: 24,
59
+ borderRadius: 16,
60
+ gap: 8,
61
+ minWidth: 120,
62
+ },
63
+ shareButton: {
64
+ backgroundColor: tokens.colors.primary,
65
+ },
66
+ deleteButton: {
67
+ backgroundColor: tokens.colors.error + '10',
68
+ },
69
+ buttonText: {
70
+ fontWeight: '700',
71
+ color: '#fff',
72
+ fontSize: 15,
73
+ },
74
+ });
@@ -0,0 +1,81 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
4
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+
7
+ interface DetailHeaderProps {
8
+ readonly title: string;
9
+ readonly date: string;
10
+ readonly onClose: () => void;
11
+ }
12
+
13
+ export const DetailHeader: React.FC<DetailHeaderProps> = ({ title, date, onClose }) => {
14
+ const tokens = useAppDesignTokens();
15
+ const insets = useSafeAreaInsets();
16
+ const styles = useStyles(tokens, insets);
17
+
18
+ return (
19
+ <View style={styles.headerContainer}>
20
+ <TouchableOpacity style={styles.closeButton} onPress={onClose}>
21
+ <AtomicIcon name="arrow-back" size={24} customColor={tokens.colors.textPrimary} />
22
+ </TouchableOpacity>
23
+
24
+ <View style={styles.titleContainer}>
25
+ <AtomicText style={styles.title} numberOfLines={1}>{title}</AtomicText>
26
+ <View style={styles.dateBadge}>
27
+ <AtomicIcon name="calendar-outline" size={12} customColor={tokens.colors.primary} />
28
+ <AtomicText style={styles.dateText}>{date}</AtomicText>
29
+ </View>
30
+ </View>
31
+
32
+ <View style={styles.placeholder} />
33
+ </View>
34
+ );
35
+ };
36
+
37
+ const useStyles = (tokens: any, insets: any) => StyleSheet.create({
38
+ headerContainer: {
39
+ flexDirection: 'row',
40
+ alignItems: 'center',
41
+ paddingTop: insets.top + tokens.spacing.sm,
42
+ paddingBottom: tokens.spacing.md,
43
+ paddingHorizontal: tokens.spacing.md,
44
+ backgroundColor: tokens.colors.backgroundPrimary,
45
+ borderBottomWidth: 1,
46
+ borderBottomColor: tokens.colors.border,
47
+ zIndex: 10,
48
+ },
49
+ closeButton: {
50
+ padding: tokens.spacing.xs,
51
+ marginRight: tokens.spacing.sm,
52
+ },
53
+ titleContainer: {
54
+ flex: 1,
55
+ alignItems: 'center',
56
+ },
57
+ title: {
58
+ fontSize: 18,
59
+ fontWeight: '700',
60
+ color: tokens.colors.textPrimary,
61
+ marginBottom: 4,
62
+ textAlign: 'center',
63
+ },
64
+ dateBadge: {
65
+ flexDirection: 'row',
66
+ alignItems: 'center',
67
+ gap: 4,
68
+ paddingHorizontal: 10,
69
+ paddingVertical: 4,
70
+ borderRadius: 12,
71
+ backgroundColor: tokens.colors.primary + '15',
72
+ },
73
+ dateText: {
74
+ fontSize: 12,
75
+ fontWeight: '600',
76
+ color: tokens.colors.primary,
77
+ },
78
+ placeholder: {
79
+ width: 40,
80
+ },
81
+ });
@@ -0,0 +1,46 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet, Image, Dimensions } from 'react-native';
4
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
5
+
6
+ interface DetailImageProps {
7
+ readonly uri: string;
8
+ }
9
+
10
+ const { width } = Dimensions.get('window');
11
+
12
+ export const DetailImage: React.FC<DetailImageProps> = ({ uri }) => {
13
+ const tokens = useAppDesignTokens();
14
+ const styles = useStyles(tokens);
15
+
16
+ return (
17
+ <View style={styles.container}>
18
+ <View style={styles.frame}>
19
+ <Image source={{ uri }} style={styles.image} resizeMode="cover" />
20
+ </View>
21
+ </View>
22
+ );
23
+ };
24
+
25
+ const useStyles = (tokens: any) => StyleSheet.create({
26
+ container: {
27
+ paddingHorizontal: tokens.spacing.lg,
28
+ marginVertical: tokens.spacing.lg,
29
+ },
30
+ frame: {
31
+ width: width - (tokens.spacing.lg * 2),
32
+ height: width - (tokens.spacing.lg * 2),
33
+ borderRadius: 24,
34
+ overflow: 'hidden',
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
+ },
42
+ image: {
43
+ width: '100%',
44
+ height: '100%',
45
+ },
46
+ });
@@ -0,0 +1,67 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet } from 'react-native';
4
+ import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
5
+ import { LinearGradient } from 'expo-linear-gradient';
6
+
7
+ interface DetailStoryProps {
8
+ readonly story: string;
9
+ }
10
+
11
+ export const DetailStory: React.FC<DetailStoryProps> = ({ story }) => {
12
+ const tokens = useAppDesignTokens();
13
+ const styles = useStyles(tokens);
14
+
15
+ if (!story) return null;
16
+
17
+ return (
18
+ <View style={styles.container}>
19
+ <LinearGradient
20
+ colors={[tokens.colors.primary + '15', tokens.colors.primary + '05']}
21
+ style={styles.gradient}
22
+ >
23
+ <AtomicText style={styles.quoteMark}>&quot;</AtomicText>
24
+ <AtomicText style={styles.text}>{story}</AtomicText>
25
+ <View style={styles.quoteEndRow}>
26
+ <AtomicText style={[styles.quoteMark, styles.quoteEnd]}>&quot;</AtomicText>
27
+ </View>
28
+ </LinearGradient>
29
+ </View>
30
+ );
31
+ };
32
+
33
+ const useStyles = (tokens: any) => StyleSheet.create({
34
+ container: {
35
+ paddingHorizontal: tokens.spacing.lg,
36
+ marginBottom: tokens.spacing.lg,
37
+ },
38
+ gradient: {
39
+ padding: tokens.spacing.lg,
40
+ borderRadius: 20,
41
+ borderWidth: 1,
42
+ borderColor: tokens.colors.primary + '20',
43
+ },
44
+ quoteMark: {
45
+ fontSize: 48,
46
+ lineHeight: 48,
47
+ color: tokens.colors.primary,
48
+ opacity: 0.4,
49
+ marginBottom: -16,
50
+ },
51
+ quoteEndRow: {
52
+ alignItems: 'flex-end',
53
+ marginTop: -16,
54
+ },
55
+ quoteEnd: {
56
+ marginBottom: 0,
57
+ },
58
+ text: {
59
+ fontSize: 16,
60
+ lineHeight: 26,
61
+ textAlign: 'center',
62
+ fontStyle: 'italic',
63
+ fontWeight: '500',
64
+ color: tokens.colors.textPrimary,
65
+ paddingBottom: 4,
66
+ },
67
+ });
@@ -5,18 +5,22 @@
5
5
 
6
6
  import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
- import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
8
+ import { AtomicText, AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
9
9
 
10
10
  interface EmptyStateProps {
11
11
  readonly title: string;
12
12
  readonly description: string;
13
13
  readonly icon?: string;
14
+ readonly actionLabel?: string;
15
+ readonly onAction?: () => void;
14
16
  }
15
17
 
16
18
  export function EmptyState({
17
19
  title,
18
20
  description,
19
21
  icon = "🎨",
22
+ actionLabel,
23
+ onAction,
20
24
  }: EmptyStateProps) {
21
25
  const tokens = useAppDesignTokens();
22
26
 
@@ -43,9 +47,13 @@ export function EmptyState({
43
47
  ...tokens.typography.bodyMedium,
44
48
  color: tokens.colors.textSecondary,
45
49
  textAlign: "center",
50
+ marginBottom: onAction ? tokens.spacing.xl : 0,
51
+ },
52
+ button: {
53
+ minWidth: 160,
46
54
  },
47
55
  }),
48
- [tokens],
56
+ [tokens, onAction],
49
57
  );
50
58
 
51
59
  return (
@@ -53,6 +61,14 @@ export function EmptyState({
53
61
  <AtomicText style={styles.icon}>{icon}</AtomicText>
54
62
  <AtomicText style={styles.title}>{title}</AtomicText>
55
63
  <AtomicText style={styles.description}>{description}</AtomicText>
64
+ {onAction && actionLabel && (
65
+ <AtomicButton
66
+ title={actionLabel}
67
+ onPress={onAction}
68
+ variant="primary"
69
+ style={styles.button}
70
+ />
71
+ )}
56
72
  </View>
57
73
  );
58
74
  }
@@ -8,6 +8,7 @@ interface GalleryHeaderProps {
8
8
  readonly countLabel: string;
9
9
  readonly isFiltered: boolean;
10
10
  readonly onFilterPress: () => void;
11
+ readonly style?: any;
11
12
  }
12
13
 
13
14
  export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
@@ -15,13 +16,14 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
15
16
  count,
16
17
  countLabel,
17
18
  isFiltered,
18
- onFilterPress
19
+ onFilterPress,
20
+ style,
19
21
  }) => {
20
22
  const tokens = useAppDesignTokens();
21
23
  const styles = useStyles(tokens);
22
24
 
23
25
  return (
24
- <View style={styles.headerArea}>
26
+ <View style={[styles.headerArea, style]}>
25
27
  <View>
26
28
  <AtomicText style={styles.title}>{title}</AtomicText>
27
29
  <AtomicText style={styles.subtitle}>
@@ -0,0 +1,71 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet, ScrollView } from 'react-native';
4
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
5
+ import type { Creation } from '../../domain/entities/Creation';
6
+ import { DetailHeader } from '../components/CreationDetail/DetailHeader';
7
+ import { DetailImage } from '../components/CreationDetail/DetailImage';
8
+ import { DetailStory } from '../components/CreationDetail/DetailStory';
9
+ import { DetailActions } from '../components/CreationDetail/DetailActions';
10
+
11
+ interface CreationDetailScreenProps {
12
+ readonly creation: Creation;
13
+ readonly onClose: () => void;
14
+ readonly onShare: (creation: Creation) => void;
15
+ readonly onDelete: (creation: Creation) => void;
16
+ readonly t: (key: string) => string;
17
+ }
18
+
19
+ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
20
+ creation,
21
+ onClose,
22
+ onShare,
23
+ onDelete,
24
+ t
25
+ }) => {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ // Extract data
29
+ const metadata = (creation as any).metadata || {};
30
+ const title = metadata.names || creation.type;
31
+ const story = metadata.story || metadata.description || "";
32
+ const date = metadata.date || new Date(creation.createdAt).toLocaleDateString();
33
+
34
+ const styles = useStyles(tokens);
35
+
36
+ return (
37
+ <View style={styles.container}>
38
+ <DetailHeader
39
+ title={title}
40
+ date={date}
41
+ onClose={onClose}
42
+ />
43
+
44
+ <ScrollView
45
+ contentContainerStyle={styles.scrollContent}
46
+ showsVerticalScrollIndicator={false}
47
+ >
48
+ <DetailImage uri={creation.uri} />
49
+
50
+ <DetailStory story={story} />
51
+
52
+ <DetailActions
53
+ onShare={() => onShare(creation)}
54
+ onDelete={() => onDelete(creation)}
55
+ shareLabel={t("result.shareButton") || "Share"}
56
+ deleteLabel={t("common.delete") || "Delete"}
57
+ />
58
+ </ScrollView>
59
+ </View>
60
+ );
61
+ };
62
+
63
+ const useStyles = (tokens: any) => StyleSheet.create({
64
+ container: {
65
+ flex: 1,
66
+ backgroundColor: tokens.colors.backgroundPrimary,
67
+ },
68
+ scrollContent: {
69
+ paddingBottom: tokens.spacing.xxl,
70
+ },
71
+ });
@@ -23,6 +23,7 @@ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
23
23
  import { GalleryHeader } from "../components/GalleryHeader";
24
24
 
25
25
  import { useAlert } from "@umituz/react-native-alert";
26
+ import { CreationDetailScreen } from "./CreationDetailScreen";
26
27
 
27
28
  interface CreationsGalleryScreenProps {
28
29
  readonly userId: string | null;
@@ -31,6 +32,8 @@ interface CreationsGalleryScreenProps {
31
32
  readonly t: (key: string) => string;
32
33
  readonly enableEditing?: boolean;
33
34
  readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
35
+ readonly onEmptyAction?: () => void;
36
+ readonly emptyActionLabel?: string;
34
37
  }
35
38
 
36
39
  export function CreationsGalleryScreen({
@@ -40,13 +43,19 @@ export function CreationsGalleryScreen({
40
43
  t,
41
44
  enableEditing = false,
42
45
  onImageEdit,
46
+ onEmptyAction,
47
+ emptyActionLabel,
43
48
  }: CreationsGalleryScreenProps) {
44
49
  const tokens = useAppDesignTokens();
45
50
  const insets = useSafeAreaInsets();
46
51
  const { share } = useSharing();
47
52
  const alert = useAlert();
53
+
54
+ // State
48
55
  const [viewerVisible, setViewerVisible] = useState(false);
49
56
  const [viewerIndex, setViewerIndex] = useState(0);
57
+ const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
58
+
50
59
  const filterSheetRef = React.useRef<BottomSheetModalRef>(null);
51
60
 
52
61
  const { data: creations, isLoading, refetch } = useCreations({
@@ -61,7 +70,6 @@ export function CreationsGalleryScreen({
61
70
  const allCategories = useMemo(() => {
62
71
  const categories: FilterCategory[] = [];
63
72
 
64
- // Add dynamic types category if types exist
65
73
  if (config.types.length > 0) {
66
74
  categories.push({
67
75
  id: 'type',
@@ -82,16 +90,13 @@ export function CreationsGalleryScreen({
82
90
  return categories;
83
91
  }, [config.types, config.filterCategories, t]);
84
92
 
85
- const handleView = useCallback(
86
- (creation: Creation) => {
87
- const index = filtered.findIndex((c) => c.id === creation.id);
88
- if (index >= 0) {
89
- setViewerIndex(index);
90
- setViewerVisible(true);
91
- }
92
- },
93
- [filtered],
94
- );
93
+ const handleView = useCallback((creation: Creation) => {
94
+ setSelectedCreation(creation);
95
+ }, []);
96
+
97
+ const handleCloseDetail = useCallback(() => {
98
+ setSelectedCreation(null);
99
+ }, []);
95
100
 
96
101
  const handleShare = useCallback(
97
102
  async (creation: Creation) => {
@@ -125,7 +130,10 @@ export function CreationsGalleryScreen({
125
130
  label: t("common.delete"),
126
131
  style: 'destructive',
127
132
  variant: 'danger',
128
- onPress: () => deleteMutation.mutate(creation.id),
133
+ onPress: () => {
134
+ deleteMutation.mutate(creation.id);
135
+ setSelectedCreation(null);
136
+ },
129
137
  },
130
138
  ]
131
139
  });
@@ -169,7 +177,7 @@ export function CreationsGalleryScreen({
169
177
  <CreationCard
170
178
  creation={item}
171
179
  types={config.types}
172
- onView={handleView}
180
+ onView={() => handleView(item)}
173
181
  onShare={handleShare}
174
182
  onDelete={handleDelete}
175
183
  />
@@ -177,12 +185,27 @@ export function CreationsGalleryScreen({
177
185
  [config.types, handleView, handleShare, handleDelete],
178
186
  );
179
187
 
188
+ // If a creation is selected, show detail screen (simulating a stack push)
189
+ if (selectedCreation) {
190
+ return (
191
+ <CreationDetailScreen
192
+ creation={selectedCreation}
193
+ onClose={handleCloseDetail}
194
+ onShare={handleShare}
195
+ onDelete={handleDelete}
196
+ t={t}
197
+ />
198
+ );
199
+ }
200
+
180
201
  if (!isLoading && (!creations || creations.length === 0)) {
181
202
  return (
182
203
  <View style={styles.container}>
183
204
  <EmptyState
184
205
  title={t(config.translations.empty)}
185
206
  description={t(config.translations.emptyDescription)}
207
+ actionLabel={emptyActionLabel}
208
+ onAction={onEmptyAction}
186
209
  />
187
210
  </View>
188
211
  );
@@ -196,6 +219,7 @@ export function CreationsGalleryScreen({
196
219
  countLabel={t(config.translations.photoCount) || 'photos'}
197
220
  isFiltered={isFiltered}
198
221
  onFilterPress={() => filterSheetRef.current?.present()}
222
+ style={{ paddingTop: insets.top }}
199
223
  />
200
224
 
201
225
  <FlatList
@@ -211,6 +235,8 @@ export function CreationsGalleryScreen({
211
235
  />
212
236
  }
213
237
  />
238
+
239
+ {/* Optional: Keep ImageGallery for quick preview if needed, or rely on Detail Screen */}
214
240
  <ImageGallery
215
241
  images={viewerImages}
216
242
  visible={viewerVisible}