@umituz/react-native-ai-creations 1.0.0
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/LICENSE +21 -0
- package/README.md +92 -0
- package/package.json +55 -0
- package/src/domain/entities/Creation.ts +43 -0
- package/src/domain/entities/index.ts +6 -0
- package/src/domain/repositories/ICreationsRepository.ts +16 -0
- package/src/domain/repositories/index.ts +5 -0
- package/src/domain/value-objects/CreationsConfig.ts +48 -0
- package/src/domain/value-objects/index.ts +10 -0
- package/src/index.ts +68 -0
- package/src/infrastructure/adapters/createRepository.ts +15 -0
- package/src/infrastructure/adapters/index.ts +5 -0
- package/src/infrastructure/repositories/CreationsRepository.ts +100 -0
- package/src/infrastructure/repositories/index.ts +5 -0
- package/src/presentation/components/CreationCard.tsx +127 -0
- package/src/presentation/components/CreationThumbnail.tsx +41 -0
- package/src/presentation/components/CreationsHomeCard.tsx +176 -0
- package/src/presentation/components/EmptyState.tsx +58 -0
- package/src/presentation/components/FilterChips.tsx +103 -0
- package/src/presentation/components/index.ts +9 -0
- package/src/presentation/hooks/index.ts +7 -0
- package/src/presentation/hooks/useCreations.ts +44 -0
- package/src/presentation/hooks/useCreationsFilter.ts +46 -0
- package/src/presentation/hooks/useDeleteCreation.ts +51 -0
- package/src/presentation/screens/CreationsGalleryScreen.tsx +154 -0
- package/src/presentation/screens/index.ts +5 -0
- package/src/types.d.ts +72 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationsHomeCard Component
|
|
3
|
+
* Shows user's creations preview on home screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useCallback } from "react";
|
|
9
|
+
import { View, TouchableOpacity, FlatList, StyleSheet } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
AtomicText,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
useAppDesignTokens,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
16
|
+
import { CreationThumbnail } from "./CreationThumbnail";
|
|
17
|
+
|
|
18
|
+
interface CreationsHomeCardProps {
|
|
19
|
+
readonly creations: Creation[] | undefined;
|
|
20
|
+
readonly isLoading: boolean;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly countLabel: string;
|
|
23
|
+
readonly loadingLabel: string;
|
|
24
|
+
readonly maxThumbnails?: number;
|
|
25
|
+
readonly onPress: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CreationsHomeCard({
|
|
29
|
+
creations,
|
|
30
|
+
isLoading,
|
|
31
|
+
title,
|
|
32
|
+
countLabel,
|
|
33
|
+
loadingLabel,
|
|
34
|
+
maxThumbnails = 4,
|
|
35
|
+
onPress,
|
|
36
|
+
}: CreationsHomeCardProps) {
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
|
|
39
|
+
if (__DEV__) {
|
|
40
|
+
console.log("[CreationsHomeCard] Render:", {
|
|
41
|
+
isLoading,
|
|
42
|
+
count: creations?.length ?? 0,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = useMemo(
|
|
47
|
+
() =>
|
|
48
|
+
StyleSheet.create({
|
|
49
|
+
container: {
|
|
50
|
+
backgroundColor: tokens.colors.surface,
|
|
51
|
+
borderRadius: tokens.spacing.md,
|
|
52
|
+
padding: tokens.spacing.md,
|
|
53
|
+
},
|
|
54
|
+
header: {
|
|
55
|
+
flexDirection: "row",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
marginBottom: tokens.spacing.md,
|
|
59
|
+
},
|
|
60
|
+
headerLeft: {
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
gap: tokens.spacing.sm,
|
|
64
|
+
},
|
|
65
|
+
icon: {
|
|
66
|
+
fontSize: 20,
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
...tokens.typography.bodyLarge,
|
|
70
|
+
fontWeight: "600",
|
|
71
|
+
color: tokens.colors.textPrimary,
|
|
72
|
+
},
|
|
73
|
+
viewAll: {
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
gap: tokens.spacing.xs,
|
|
77
|
+
},
|
|
78
|
+
count: {
|
|
79
|
+
...tokens.typography.bodySmall,
|
|
80
|
+
color: tokens.colors.textSecondary,
|
|
81
|
+
},
|
|
82
|
+
thumbnailList: {
|
|
83
|
+
gap: tokens.spacing.sm,
|
|
84
|
+
},
|
|
85
|
+
loadingText: {
|
|
86
|
+
...tokens.typography.bodySmall,
|
|
87
|
+
color: tokens.colors.textSecondary,
|
|
88
|
+
textAlign: "center",
|
|
89
|
+
padding: tokens.spacing.md,
|
|
90
|
+
},
|
|
91
|
+
moreBadge: {
|
|
92
|
+
width: 72,
|
|
93
|
+
height: 72,
|
|
94
|
+
borderRadius: tokens.spacing.sm,
|
|
95
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
},
|
|
99
|
+
moreText: {
|
|
100
|
+
...tokens.typography.bodySmall,
|
|
101
|
+
fontWeight: "600",
|
|
102
|
+
color: tokens.colors.primary,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
[tokens],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const displayItems = useMemo(() => {
|
|
109
|
+
if (!creations) return [];
|
|
110
|
+
return creations.slice(0, maxThumbnails);
|
|
111
|
+
}, [creations, maxThumbnails]);
|
|
112
|
+
|
|
113
|
+
const renderItem = useCallback(
|
|
114
|
+
({ item, index }: { item: Creation; index: number }) => {
|
|
115
|
+
const isLast = index === maxThumbnails - 1;
|
|
116
|
+
const hasMore = creations && creations.length > maxThumbnails;
|
|
117
|
+
|
|
118
|
+
if (isLast && hasMore) {
|
|
119
|
+
return (
|
|
120
|
+
<TouchableOpacity style={styles.moreBadge} onPress={onPress}>
|
|
121
|
+
<AtomicText style={styles.moreText}>
|
|
122
|
+
+{creations.length - maxThumbnails + 1}
|
|
123
|
+
</AtomicText>
|
|
124
|
+
</TouchableOpacity>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return <CreationThumbnail uri={item.uri} onPress={onPress} />;
|
|
129
|
+
},
|
|
130
|
+
[styles, creations, maxThumbnails, onPress],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (isLoading) {
|
|
134
|
+
return (
|
|
135
|
+
<View style={styles.container}>
|
|
136
|
+
<AtomicText style={styles.loadingText}>{loadingLabel}</AtomicText>
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!creations || creations.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const count = creations.length;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
style={styles.container}
|
|
150
|
+
onPress={onPress}
|
|
151
|
+
activeOpacity={0.8}
|
|
152
|
+
>
|
|
153
|
+
<View style={styles.header}>
|
|
154
|
+
<View style={styles.headerLeft}>
|
|
155
|
+
<AtomicText style={styles.icon}>🎨</AtomicText>
|
|
156
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
157
|
+
</View>
|
|
158
|
+
<TouchableOpacity style={styles.viewAll} onPress={onPress}>
|
|
159
|
+
<AtomicText style={styles.count}>
|
|
160
|
+
{countLabel.replace("{{count}}", String(count))}
|
|
161
|
+
</AtomicText>
|
|
162
|
+
<AtomicIcon name="chevron-forward" size="sm" color="primary" />
|
|
163
|
+
</TouchableOpacity>
|
|
164
|
+
</View>
|
|
165
|
+
<FlatList
|
|
166
|
+
data={displayItems}
|
|
167
|
+
renderItem={renderItem}
|
|
168
|
+
keyExtractor={(item) => item.id}
|
|
169
|
+
horizontal
|
|
170
|
+
showsHorizontalScrollIndicator={false}
|
|
171
|
+
contentContainerStyle={styles.thumbnailList}
|
|
172
|
+
scrollEnabled={false}
|
|
173
|
+
/>
|
|
174
|
+
</TouchableOpacity>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyState Component
|
|
3
|
+
* Displays when no creations exist
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
interface EmptyStateProps {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly icon?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EmptyState({
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
icon = "🎨",
|
|
20
|
+
}: EmptyStateProps) {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
|
|
23
|
+
const styles = useMemo(
|
|
24
|
+
() =>
|
|
25
|
+
StyleSheet.create({
|
|
26
|
+
container: {
|
|
27
|
+
flex: 1,
|
|
28
|
+
justifyContent: "center",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
padding: tokens.spacing.xl,
|
|
31
|
+
},
|
|
32
|
+
icon: {
|
|
33
|
+
fontSize: 48,
|
|
34
|
+
marginBottom: tokens.spacing.md,
|
|
35
|
+
},
|
|
36
|
+
title: {
|
|
37
|
+
...tokens.typography.headingSmall,
|
|
38
|
+
color: tokens.colors.textPrimary,
|
|
39
|
+
textAlign: "center",
|
|
40
|
+
marginBottom: tokens.spacing.sm,
|
|
41
|
+
},
|
|
42
|
+
description: {
|
|
43
|
+
...tokens.typography.bodyMedium,
|
|
44
|
+
color: tokens.colors.textSecondary,
|
|
45
|
+
textAlign: "center",
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
[tokens],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<View style={styles.container}>
|
|
53
|
+
<AtomicText style={styles.icon}>{icon}</AtomicText>
|
|
54
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
55
|
+
<AtomicText style={styles.description}>{description}</AtomicText>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FilterChips Component
|
|
3
|
+
* Displays filter chips for creation types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
|
|
8
|
+
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
10
|
+
|
|
11
|
+
interface FilterChipsProps {
|
|
12
|
+
readonly types: readonly CreationType[];
|
|
13
|
+
readonly availableTypes: string[];
|
|
14
|
+
readonly selectedType: string;
|
|
15
|
+
readonly allLabel: string;
|
|
16
|
+
readonly onSelect: (type: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FilterChips({
|
|
20
|
+
types,
|
|
21
|
+
availableTypes,
|
|
22
|
+
selectedType,
|
|
23
|
+
allLabel,
|
|
24
|
+
onSelect,
|
|
25
|
+
}: FilterChipsProps) {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
const styles = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
marginBottom: tokens.spacing.md,
|
|
33
|
+
},
|
|
34
|
+
scrollContent: {
|
|
35
|
+
paddingHorizontal: tokens.spacing.md,
|
|
36
|
+
gap: tokens.spacing.sm,
|
|
37
|
+
flexDirection: "row",
|
|
38
|
+
},
|
|
39
|
+
chip: {
|
|
40
|
+
paddingHorizontal: tokens.spacing.md,
|
|
41
|
+
paddingVertical: tokens.spacing.sm,
|
|
42
|
+
borderRadius: tokens.spacing.lg,
|
|
43
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
44
|
+
},
|
|
45
|
+
chipSelected: {
|
|
46
|
+
backgroundColor: tokens.colors.primary,
|
|
47
|
+
},
|
|
48
|
+
chipText: {
|
|
49
|
+
...tokens.typography.bodySmall,
|
|
50
|
+
color: tokens.colors.textSecondary,
|
|
51
|
+
},
|
|
52
|
+
chipTextSelected: {
|
|
53
|
+
color: tokens.colors.textInverse,
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
[tokens],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const visibleTypes = types.filter((t) => availableTypes.includes(t.id));
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={styles.container}>
|
|
63
|
+
<ScrollView
|
|
64
|
+
horizontal
|
|
65
|
+
showsHorizontalScrollIndicator={false}
|
|
66
|
+
contentContainerStyle={styles.scrollContent}
|
|
67
|
+
>
|
|
68
|
+
<TouchableOpacity
|
|
69
|
+
style={[styles.chip, selectedType === "all" && styles.chipSelected]}
|
|
70
|
+
onPress={() => onSelect("all")}
|
|
71
|
+
>
|
|
72
|
+
<AtomicText
|
|
73
|
+
style={[
|
|
74
|
+
styles.chipText,
|
|
75
|
+
selectedType === "all" && styles.chipTextSelected,
|
|
76
|
+
]}
|
|
77
|
+
>
|
|
78
|
+
{allLabel}
|
|
79
|
+
</AtomicText>
|
|
80
|
+
</TouchableOpacity>
|
|
81
|
+
{visibleTypes.map((type) => (
|
|
82
|
+
<TouchableOpacity
|
|
83
|
+
key={type.id}
|
|
84
|
+
style={[
|
|
85
|
+
styles.chip,
|
|
86
|
+
selectedType === type.id && styles.chipSelected,
|
|
87
|
+
]}
|
|
88
|
+
onPress={() => onSelect(type.id)}
|
|
89
|
+
>
|
|
90
|
+
<AtomicText
|
|
91
|
+
style={[
|
|
92
|
+
styles.chipText,
|
|
93
|
+
selectedType === type.id && styles.chipTextSelected,
|
|
94
|
+
]}
|
|
95
|
+
>
|
|
96
|
+
{type.icon} {type.labelKey}
|
|
97
|
+
</AtomicText>
|
|
98
|
+
</TouchableOpacity>
|
|
99
|
+
))}
|
|
100
|
+
</ScrollView>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { CreationThumbnail } from "./CreationThumbnail";
|
|
6
|
+
export { CreationCard } from "./CreationCard";
|
|
7
|
+
export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
8
|
+
export { FilterChips } from "./FilterChips";
|
|
9
|
+
export { EmptyState } from "./EmptyState";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreations Hook
|
|
3
|
+
* Fetches user's creations from repository
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
import { useQuery } from "@tanstack/react-query";
|
|
9
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
10
|
+
|
|
11
|
+
const CACHE_CONFIG = {
|
|
12
|
+
staleTime: 5 * 60 * 1000,
|
|
13
|
+
gcTime: 30 * 60 * 1000,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface UseCreationsProps {
|
|
17
|
+
readonly userId: string | null;
|
|
18
|
+
readonly repository: ICreationsRepository;
|
|
19
|
+
readonly enabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useCreations({
|
|
23
|
+
userId,
|
|
24
|
+
repository,
|
|
25
|
+
enabled = true,
|
|
26
|
+
}: UseCreationsProps) {
|
|
27
|
+
const result = useQuery({
|
|
28
|
+
queryKey: ["creations", userId ?? ""],
|
|
29
|
+
queryFn: () => repository.getAll(userId!),
|
|
30
|
+
enabled: !!userId && enabled,
|
|
31
|
+
staleTime: CACHE_CONFIG.staleTime,
|
|
32
|
+
gcTime: CACHE_CONFIG.gcTime,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.log("[useCreations] State:", {
|
|
37
|
+
isLoading: result.isLoading,
|
|
38
|
+
count: result.data?.length ?? 0,
|
|
39
|
+
enabled: !!userId && enabled,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreationsFilter Hook
|
|
3
|
+
* Handles filtering of creations by type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useMemo, useCallback } from "react";
|
|
7
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
+
|
|
9
|
+
const ALL_FILTER = "all";
|
|
10
|
+
|
|
11
|
+
interface UseCreationsFilterProps {
|
|
12
|
+
readonly creations: Creation[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCreationsFilter({ creations }: UseCreationsFilterProps) {
|
|
16
|
+
const [selectedType, setSelectedType] = useState<string>(ALL_FILTER);
|
|
17
|
+
|
|
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]);
|
|
23
|
+
|
|
24
|
+
const filtered = useMemo(() => {
|
|
25
|
+
if (!creations) return [];
|
|
26
|
+
if (selectedType === ALL_FILTER) return creations;
|
|
27
|
+
return creations.filter((c) => c.type === selectedType);
|
|
28
|
+
}, [creations, selectedType]);
|
|
29
|
+
|
|
30
|
+
const selectType = useCallback((type: string) => {
|
|
31
|
+
setSelectedType(type);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const resetFilter = useCallback(() => {
|
|
35
|
+
setSelectedType(ALL_FILTER);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
filtered,
|
|
40
|
+
selectedType,
|
|
41
|
+
availableTypes,
|
|
42
|
+
selectType,
|
|
43
|
+
resetFilter,
|
|
44
|
+
isFiltered: selectedType !== ALL_FILTER,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDeleteCreation Hook
|
|
3
|
+
* Handles 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 UseDeleteCreationProps {
|
|
11
|
+
readonly userId: string | null;
|
|
12
|
+
readonly repository: ICreationsRepository;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useDeleteCreation({
|
|
16
|
+
userId,
|
|
17
|
+
repository,
|
|
18
|
+
}: UseDeleteCreationProps) {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (creationId: string) => {
|
|
23
|
+
if (!userId) return false;
|
|
24
|
+
return repository.delete(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) => old?.filter((c) => c.id !== creationId) ?? [],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { previous };
|
|
44
|
+
},
|
|
45
|
+
onError: (_err, _id, rollback) => {
|
|
46
|
+
if (userId && rollback?.previous) {
|
|
47
|
+
queryClient.setQueryData(["creations", userId], rollback.previous);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationsGalleryScreen
|
|
3
|
+
* Full gallery view with filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useCallback } from "react";
|
|
9
|
+
import { View, FlatList, StyleSheet, RefreshControl, Alert } from "react-native";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
11
|
+
import { useSharing } from "@umituz/react-native-sharing";
|
|
12
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
13
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
14
|
+
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
15
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
16
|
+
import { useCreations } from "../hooks/useCreations";
|
|
17
|
+
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
18
|
+
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
19
|
+
import { CreationCard } from "../components/CreationCard";
|
|
20
|
+
import { FilterChips } from "../components/FilterChips";
|
|
21
|
+
import { EmptyState } from "../components/EmptyState";
|
|
22
|
+
|
|
23
|
+
interface CreationsGalleryScreenProps {
|
|
24
|
+
readonly userId: string | null;
|
|
25
|
+
readonly repository: ICreationsRepository;
|
|
26
|
+
readonly config: CreationsConfig;
|
|
27
|
+
readonly t: (key: string) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CreationsGalleryScreen({
|
|
31
|
+
userId,
|
|
32
|
+
repository,
|
|
33
|
+
config,
|
|
34
|
+
t,
|
|
35
|
+
}: CreationsGalleryScreenProps) {
|
|
36
|
+
const tokens = useAppDesignTokens();
|
|
37
|
+
const insets = useSafeAreaInsets();
|
|
38
|
+
const { share } = useSharing();
|
|
39
|
+
|
|
40
|
+
const { data: creations, isLoading, refetch } = useCreations({
|
|
41
|
+
userId,
|
|
42
|
+
repository,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
46
|
+
const { filtered, selectedType, availableTypes, selectType } =
|
|
47
|
+
useCreationsFilter({ creations });
|
|
48
|
+
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.log("[CreationsGalleryScreen] Render:", {
|
|
51
|
+
count: creations?.length ?? 0,
|
|
52
|
+
filtered: filtered.length,
|
|
53
|
+
selectedType,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleShare = useCallback(
|
|
58
|
+
async (creation: Creation) => {
|
|
59
|
+
try {
|
|
60
|
+
await share(creation.uri, {
|
|
61
|
+
dialogTitle: t(config.translations.title),
|
|
62
|
+
mimeType: "image/jpeg",
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Silent fail
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[share, t, config.translations.title],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleDelete = useCallback(
|
|
72
|
+
(creation: Creation) => {
|
|
73
|
+
Alert.alert(
|
|
74
|
+
t(config.translations.deleteTitle),
|
|
75
|
+
t(config.translations.deleteMessage),
|
|
76
|
+
[
|
|
77
|
+
{ text: t("common.cancel"), style: "cancel" },
|
|
78
|
+
{
|
|
79
|
+
text: t("common.delete"),
|
|
80
|
+
style: "destructive",
|
|
81
|
+
onPress: () => deleteMutation.mutate(creation.id),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
[t, config.translations, deleteMutation],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const styles = useMemo(
|
|
90
|
+
() =>
|
|
91
|
+
StyleSheet.create({
|
|
92
|
+
container: {
|
|
93
|
+
flex: 1,
|
|
94
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
95
|
+
},
|
|
96
|
+
list: {
|
|
97
|
+
padding: tokens.spacing.md,
|
|
98
|
+
paddingBottom: insets.bottom + tokens.spacing.xl,
|
|
99
|
+
gap: tokens.spacing.md,
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
[tokens, insets.bottom],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const renderItem = useCallback(
|
|
106
|
+
({ item }: { item: Creation }) => (
|
|
107
|
+
<CreationCard
|
|
108
|
+
creation={item}
|
|
109
|
+
types={config.types}
|
|
110
|
+
onShare={handleShare}
|
|
111
|
+
onDelete={handleDelete}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
114
|
+
[config.types, handleShare, handleDelete],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!isLoading && (!creations || creations.length === 0)) {
|
|
118
|
+
return (
|
|
119
|
+
<View style={styles.container}>
|
|
120
|
+
<EmptyState
|
|
121
|
+
title={t(config.translations.empty)}
|
|
122
|
+
description={t(config.translations.emptyDescription)}
|
|
123
|
+
/>
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<View style={styles.container}>
|
|
130
|
+
{config.types.length > 0 && availableTypes.length > 1 && (
|
|
131
|
+
<FilterChips
|
|
132
|
+
types={config.types}
|
|
133
|
+
availableTypes={availableTypes}
|
|
134
|
+
selectedType={selectedType}
|
|
135
|
+
allLabel={t(config.translations.filterAll)}
|
|
136
|
+
onSelect={selectType}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
<FlatList
|
|
140
|
+
data={filtered}
|
|
141
|
+
renderItem={renderItem}
|
|
142
|
+
keyExtractor={(item) => item.id}
|
|
143
|
+
contentContainerStyle={styles.list}
|
|
144
|
+
refreshControl={
|
|
145
|
+
<RefreshControl
|
|
146
|
+
refreshing={isLoading}
|
|
147
|
+
onRefresh={refetch}
|
|
148
|
+
tintColor={tokens.colors.primary}
|
|
149
|
+
/>
|
|
150
|
+
}
|
|
151
|
+
/>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
}
|