@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 +5 -4
- package/src/presentation/components/CreationDetail/DetailActions.tsx +74 -0
- package/src/presentation/components/CreationDetail/DetailHeader.tsx +81 -0
- package/src/presentation/components/CreationDetail/DetailImage.tsx +46 -0
- package/src/presentation/components/CreationDetail/DetailStory.tsx +67 -0
- package/src/presentation/components/EmptyState.tsx +18 -2
- package/src/presentation/components/GalleryHeader.tsx +4 -2
- package/src/presentation/screens/CreationDetailScreen.tsx +71 -0
- package/src/presentation/screens/CreationsGalleryScreen.tsx +39 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-creations",
|
|
3
|
-
"version": "1.2.
|
|
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
|
-
"
|
|
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}>"</AtomicText>
|
|
24
|
+
<AtomicText style={styles.text}>{story}</AtomicText>
|
|
25
|
+
<View style={styles.quoteEndRow}>
|
|
26
|
+
<AtomicText style={[styles.quoteMark, styles.quoteEnd]}>"</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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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: () =>
|
|
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}
|