@umituz/react-native-ai-generation-content 1.12.25 → 1.12.29

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.
Files changed (57) hide show
  1. package/package.json +3 -2
  2. package/src/domains/creations/application/services/CreationsService.ts +73 -0
  3. package/src/domains/creations/domain/entities/Creation.ts +60 -0
  4. package/src/domains/creations/domain/entities/index.ts +6 -0
  5. package/src/domains/creations/domain/repositories/ICreationsRepository.ts +23 -0
  6. package/src/domains/creations/domain/repositories/index.ts +5 -0
  7. package/src/domains/creations/domain/services/ICreationsStorageService.ts +13 -0
  8. package/src/domains/creations/domain/value-objects/CreationsConfig.ts +75 -0
  9. package/src/domains/creations/domain/value-objects/index.ts +12 -0
  10. package/src/domains/creations/index.ts +84 -0
  11. package/src/domains/creations/infrastructure/adapters/createRepository.ts +54 -0
  12. package/src/domains/creations/infrastructure/adapters/index.ts +5 -0
  13. package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +241 -0
  14. package/src/domains/creations/infrastructure/repositories/index.ts +8 -0
  15. package/src/domains/creations/infrastructure/services/CreationsStorageService.ts +49 -0
  16. package/src/domains/creations/presentation/components/CreationCard.tsx +136 -0
  17. package/src/domains/creations/presentation/components/CreationDetail/DetailActions.tsx +76 -0
  18. package/src/domains/creations/presentation/components/CreationDetail/DetailHeader.tsx +81 -0
  19. package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +41 -0
  20. package/src/domains/creations/presentation/components/CreationDetail/DetailStory.tsx +67 -0
  21. package/src/domains/creations/presentation/components/CreationDetail/index.ts +4 -0
  22. package/src/domains/creations/presentation/components/CreationImageViewer.tsx +43 -0
  23. package/src/domains/creations/presentation/components/CreationThumbnail.tsx +63 -0
  24. package/src/domains/creations/presentation/components/CreationsGrid.tsx +75 -0
  25. package/src/domains/creations/presentation/components/CreationsHomeCard.tsx +176 -0
  26. package/src/domains/creations/presentation/components/EmptyState.tsx +82 -0
  27. package/src/domains/creations/presentation/components/FilterBottomSheet.tsx +160 -0
  28. package/src/domains/creations/presentation/components/FilterChips.tsx +105 -0
  29. package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +87 -0
  30. package/src/domains/creations/presentation/components/GalleryHeader.tsx +106 -0
  31. package/src/domains/creations/presentation/components/index.ts +20 -0
  32. package/src/domains/creations/presentation/hooks/index.ts +7 -0
  33. package/src/domains/creations/presentation/hooks/useCreations.ts +38 -0
  34. package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +77 -0
  35. package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +51 -0
  36. package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +78 -0
  37. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +194 -0
  38. package/src/domains/creations/presentation/screens/index.ts +5 -0
  39. package/src/domains/creations/presentation/utils/filterUtils.ts +52 -0
  40. package/src/domains/creations/types.d.ts +42 -0
  41. package/src/domains/prompts/infrastructure/services/AIServiceProcessor.ts +142 -0
  42. package/src/domains/prompts/presentation/hooks/useAIServices.ts +15 -132
  43. package/src/features/background/presentation/components/ComparisonSlider.tsx +2 -2
  44. package/src/features/background/presentation/components/ErrorDisplay.tsx +2 -2
  45. package/src/features/background/presentation/components/GenerateButton.tsx +0 -2
  46. package/src/features/background/presentation/components/ImagePicker.tsx +2 -2
  47. package/src/features/background/presentation/components/ResultDisplay.tsx +2 -2
  48. package/src/index.ts +6 -0
  49. package/src/infrastructure/services/generation-orchestrator.service.ts +25 -162
  50. package/src/infrastructure/services/job-poller.ts +103 -0
  51. package/src/infrastructure/services/progress-manager.ts +58 -0
  52. package/src/infrastructure/services/provider-validator.ts +52 -0
  53. package/src/presentation/components/GenerationProgressContent.tsx +4 -4
  54. package/src/presentation/components/PendingJobCard.tsx +2 -2
  55. package/src/presentation/components/PendingJobCardActions.tsx +2 -2
  56. package/src/presentation/components/result/GenerationResultContent.tsx +2 -3
  57. package/src/presentation/hooks/usePhotoGeneration.ts +7 -5
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { View, TouchableOpacity, StyleSheet, type ViewStyle } from 'react-native';
3
+ import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
4
+
5
+ interface GalleryHeaderProps {
6
+ readonly title: string;
7
+ readonly count: number;
8
+ readonly countLabel: string;
9
+ readonly isFiltered: boolean;
10
+ readonly onFilterPress: () => void;
11
+ readonly filterLabel?: string;
12
+ readonly filterIcon?: string;
13
+ readonly style?: ViewStyle;
14
+ }
15
+
16
+ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
17
+ title,
18
+ count,
19
+ countLabel,
20
+ isFiltered,
21
+ onFilterPress,
22
+ filterLabel = 'Filter',
23
+ filterIcon = 'filter-outline',
24
+ style,
25
+ }) => {
26
+ const tokens = useAppDesignTokens();
27
+ const styles = useStyles(tokens);
28
+
29
+ return (
30
+ <View style={[styles.headerArea, style]}>
31
+ <View>
32
+ <AtomicText style={styles.title}>{title}</AtomicText>
33
+ <AtomicText style={styles.subtitle}>
34
+ {count} {countLabel}
35
+ </AtomicText>
36
+ </View>
37
+ <TouchableOpacity
38
+ onPress={onFilterPress}
39
+ style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
40
+ activeOpacity={0.7}
41
+ >
42
+ <AtomicIcon
43
+ name={filterIcon}
44
+ size="sm"
45
+ color={isFiltered ? "primary" : "secondary"}
46
+ />
47
+ <AtomicText style={[styles.filterText, { color: isFiltered ? tokens.colors.primary : tokens.colors.textSecondary }]}>
48
+ {filterLabel}
49
+ </AtomicText>
50
+ {isFiltered && (
51
+ <View style={styles.badge} />
52
+ )}
53
+ </TouchableOpacity>
54
+ </View>
55
+ );
56
+ };
57
+
58
+ const useStyles = (tokens: DesignTokens) => StyleSheet.create({
59
+ headerArea: {
60
+ flexDirection: "row",
61
+ alignItems: "center",
62
+ justifyContent: 'space-between',
63
+ paddingHorizontal: tokens.spacing.md,
64
+ paddingVertical: tokens.spacing.sm,
65
+ marginBottom: tokens.spacing.sm,
66
+ },
67
+ title: {
68
+ fontSize: 20,
69
+ fontWeight: "700",
70
+ color: tokens.colors.textPrimary,
71
+ marginBottom: 4,
72
+ },
73
+ subtitle: {
74
+ fontSize: 14,
75
+ color: tokens.colors.textSecondary,
76
+ opacity: 0.6
77
+ },
78
+ filterButton: {
79
+ flexDirection: 'row',
80
+ alignItems: 'center',
81
+ gap: tokens.spacing.xs,
82
+ paddingVertical: tokens.spacing.xs,
83
+ paddingHorizontal: tokens.spacing.md,
84
+ borderRadius: 999,
85
+ backgroundColor: tokens.colors.surfaceVariant,
86
+ borderWidth: 1,
87
+ borderColor: 'transparent',
88
+ },
89
+ filterButtonActive: {
90
+ backgroundColor: tokens.colors.primary + "15",
91
+ borderColor: tokens.colors.primary + "30",
92
+ },
93
+ filterText: {
94
+ fontSize: 14,
95
+ fontWeight: "500",
96
+ },
97
+ badge: {
98
+ width: 8,
99
+ height: 8,
100
+ borderRadius: 4,
101
+ backgroundColor: tokens.colors.primary,
102
+ position: 'absolute',
103
+ top: 6,
104
+ right: 6,
105
+ },
106
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Presentation Components
3
+ */
4
+
5
+ export { GalleryHeader } from "./GalleryHeader";
6
+ export { EmptyState } from "./EmptyState";
7
+ export { GalleryEmptyStates } from "./GalleryEmptyStates";
8
+ export { FilterChips } from "./FilterChips";
9
+ export { CreationsHomeCard } from "./CreationsHomeCard";
10
+ export { CreationCard } from "./CreationCard";
11
+ export { CreationThumbnail } from "./CreationThumbnail";
12
+ export { CreationImageViewer } from "./CreationImageViewer";
13
+ export { CreationsGrid } from "./CreationsGrid";
14
+ export { FilterBottomSheet, type FilterCategory, type FilterOption } from "./FilterBottomSheet";
15
+
16
+ // Detail Components
17
+ export { DetailHeader } from "./CreationDetail/DetailHeader";
18
+ export { DetailImage } from "./CreationDetail/DetailImage";
19
+ export { DetailStory } from "./CreationDetail/DetailStory";
20
+ export { DetailActions } from "./CreationDetail/DetailActions";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Presentation Hooks
3
+ */
4
+
5
+ export { useCreations } from "./useCreations";
6
+ export { useDeleteCreation } from "./useDeleteCreation";
7
+ export { useCreationsFilter } from "./useCreationsFilter";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * useCreations Hook
3
+ * Fetches user's creations from repository
4
+ */
5
+
6
+ import { useQuery } from "@tanstack/react-query";
7
+ import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
8
+ import type { Creation } from "../../domain/entities/Creation";
9
+
10
+ const CACHE_CONFIG = {
11
+ staleTime: 5 * 60 * 1000, // 5 minutes - use cache invalidation on mutations
12
+ gcTime: 30 * 60 * 1000,
13
+ };
14
+
15
+ interface UseCreationsProps {
16
+ readonly userId: string | null;
17
+ readonly repository: ICreationsRepository;
18
+ readonly enabled?: boolean;
19
+ }
20
+
21
+ export function useCreations({
22
+ userId,
23
+ repository,
24
+ enabled = true,
25
+ }: UseCreationsProps) {
26
+ return useQuery<Creation[]>({
27
+ queryKey: ["creations", userId ?? ""],
28
+ queryFn: async () => {
29
+ if (!userId) {
30
+ return [];
31
+ }
32
+ return repository.getAll(userId);
33
+ },
34
+ enabled: !!userId && enabled,
35
+ staleTime: CACHE_CONFIG.staleTime,
36
+ gcTime: CACHE_CONFIG.gcTime,
37
+ });
38
+ }
@@ -0,0 +1,77 @@
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
+ interface UseCreationsFilterProps {
10
+ readonly creations: Creation[] | undefined;
11
+ readonly defaultFilterId?: string;
12
+ }
13
+
14
+ interface CreationWithTags extends Creation {
15
+ readonly metadata?: {
16
+ readonly tags?: string[];
17
+ readonly [key: string]: unknown;
18
+ };
19
+ }
20
+
21
+ export function useCreationsFilter({
22
+ creations,
23
+ defaultFilterId = "all"
24
+ }: UseCreationsFilterProps) {
25
+ const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
26
+
27
+ const filtered = useMemo(() => {
28
+ if (!creations) return [];
29
+ if (selectedIds.includes(defaultFilterId)) return creations;
30
+
31
+ return creations.filter((c) => {
32
+ const creation = c as CreationWithTags;
33
+ return (
34
+ selectedIds.includes(creation.type) ||
35
+ selectedIds.some(id => creation.metadata?.tags?.includes(id))
36
+ );
37
+ });
38
+ }, [creations, selectedIds, defaultFilterId]);
39
+
40
+ const toggleFilter = useCallback((filterId: string, multiSelect = false) => {
41
+ setSelectedIds(prev => {
42
+ // If selecting 'all', clear everything else
43
+ if (filterId === defaultFilterId) return [defaultFilterId];
44
+
45
+ let newIds: string[];
46
+ if (!multiSelect) {
47
+ // Single select
48
+ if (prev.includes(filterId) && prev.length === 1) return prev;
49
+ newIds = [filterId];
50
+ } else {
51
+ // Multi select
52
+ if (prev.includes(filterId)) {
53
+ newIds = prev.filter(id => id !== filterId);
54
+ } else {
55
+ // Remove 'all' if present
56
+ newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
57
+ }
58
+ }
59
+
60
+ // If nothing selected, revert to 'all'
61
+ if (newIds.length === 0) return [defaultFilterId];
62
+ return newIds;
63
+ });
64
+ }, [defaultFilterId]);
65
+
66
+ const clearFilters = useCallback(() => {
67
+ setSelectedIds([defaultFilterId]);
68
+ }, [defaultFilterId]);
69
+
70
+ return {
71
+ filtered,
72
+ selectedIds,
73
+ toggleFilter,
74
+ clearFilters,
75
+ isFiltered: !selectedIds.includes(defaultFilterId),
76
+ };
77
+ }
@@ -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,78 @@
1
+
2
+ import React from 'react';
3
+ import { View, StyleSheet, ScrollView } from 'react-native';
4
+ import { useAppDesignTokens, type DesignTokens } 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
+ interface CreationMetadata {
20
+ readonly names?: string;
21
+ readonly story?: string;
22
+ readonly description?: string;
23
+ readonly date?: string;
24
+ }
25
+
26
+ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
27
+ creation,
28
+ onClose,
29
+ onShare,
30
+ onDelete,
31
+ t
32
+ }) => {
33
+ const tokens = useAppDesignTokens();
34
+
35
+ // Extract data safely
36
+ const metadata = (creation.metadata || {}) as CreationMetadata;
37
+ const title = metadata.names || creation.type;
38
+ const story = metadata.story || metadata.description || "";
39
+ const date = metadata.date || new Date(creation.createdAt).toLocaleDateString();
40
+
41
+ const styles = useStyles(tokens);
42
+
43
+ return (
44
+ <View style={styles.container}>
45
+ <DetailHeader
46
+ title={title}
47
+ date={date}
48
+ onClose={onClose}
49
+ />
50
+
51
+ <ScrollView
52
+ contentContainerStyle={styles.scrollContent}
53
+ showsVerticalScrollIndicator={false}
54
+ >
55
+ <DetailImage uri={creation.uri} />
56
+
57
+ <DetailStory story={story} />
58
+
59
+ <DetailActions
60
+ onShare={() => onShare(creation)}
61
+ onDelete={() => onDelete(creation)}
62
+ shareLabel={t("result.shareButton") || "Share"}
63
+ deleteLabel={t("common.delete") || "Delete"}
64
+ />
65
+ </ScrollView>
66
+ </View>
67
+ );
68
+ };
69
+
70
+ const useStyles = (tokens: DesignTokens) => StyleSheet.create({
71
+ container: {
72
+ flex: 1,
73
+ backgroundColor: tokens.colors.background,
74
+ },
75
+ scrollContent: {
76
+ paddingBottom: tokens.spacing.xxl,
77
+ },
78
+ });
@@ -0,0 +1,194 @@
1
+ import React, { useMemo, useCallback, useState } from "react";
2
+ import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
3
+ import { useAppDesignTokens, useAlert, AlertType, AlertMode, useSharing, type DesignTokens } from "@umituz/react-native-design-system";
4
+ import { BottomSheetModal } from '@gorhom/bottom-sheet';
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
6
+ import { useFocusEffect } from "@react-navigation/native";
7
+ import { useCreations } from "../hooks/useCreations";
8
+ import { useDeleteCreation } from "../hooks/useDeleteCreation";
9
+ import { useCreationsFilter } from "../hooks/useCreationsFilter";
10
+ import { GalleryHeader, CreationsGrid, FilterBottomSheet, CreationImageViewer, GalleryEmptyStates } from "../components";
11
+ import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
12
+ import type { Creation } from "../../domain/entities/Creation";
13
+ import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
14
+ import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
15
+ import { CreationDetailScreen } from "./CreationDetailScreen";
16
+
17
+ interface CreationsGalleryScreenProps {
18
+ readonly userId: string | null;
19
+ readonly repository: ICreationsRepository;
20
+ readonly config: CreationsConfig;
21
+ readonly t: (key: string) => string;
22
+ readonly enableEditing?: boolean;
23
+ readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
24
+ readonly onEmptyAction?: () => void;
25
+ readonly emptyActionLabel?: string;
26
+ }
27
+
28
+ export function CreationsGalleryScreen({
29
+ userId,
30
+ repository,
31
+ config,
32
+ t,
33
+ enableEditing = false,
34
+ onImageEdit,
35
+ onEmptyAction,
36
+ emptyActionLabel,
37
+ }: CreationsGalleryScreenProps) {
38
+ const tokens = useAppDesignTokens();
39
+ const insets = useSafeAreaInsets();
40
+ const { share } = useSharing();
41
+ const alert = useAlert();
42
+
43
+ const [viewerVisible, setViewerVisible] = useState(false);
44
+ const [viewerIndex, setViewerIndex] = useState(0);
45
+ const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
46
+ const filterSheetRef = React.useRef<BottomSheetModal>(null);
47
+
48
+ const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
49
+ const creations = creationsData;
50
+ const deleteMutation = useDeleteCreation({ userId, repository });
51
+ const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
52
+
53
+ // Refetch creations when screen comes into focus
54
+ useFocusEffect(
55
+ useCallback(() => {
56
+ void refetch();
57
+ }, [refetch])
58
+ );
59
+
60
+ // Prepare data for UI using utils
61
+ const translatedTypes = useMemo(() => getTranslatedTypes(config, t), [config, t]);
62
+ const allCategories = useMemo(() => getFilterCategoriesFromConfig(config, t), [config, t]);
63
+
64
+ const handleShare = useCallback((creation: Creation) => {
65
+ void share(creation.uri, { dialogTitle: t("common.share") });
66
+ }, [share, t]);
67
+
68
+ const handleDelete = useCallback((creation: Creation) => {
69
+ alert.show(
70
+ AlertType.WARNING,
71
+ AlertMode.MODAL,
72
+ t(config.translations.deleteTitle),
73
+ t(config.translations.deleteMessage),
74
+ {
75
+ actions: [
76
+ { id: 'cancel', label: t("common.cancel"), onPress: () => { } },
77
+ {
78
+ id: 'delete',
79
+ label: t("common.delete"),
80
+ style: 'destructive',
81
+ onPress: async () => {
82
+ const success = await deleteMutation.mutateAsync(creation.id);
83
+ if (success) {
84
+ setSelectedCreation(null);
85
+ }
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ );
91
+ }, [alert, config, deleteMutation, t]);
92
+
93
+ // Handle viewing a creation - shows detail screen
94
+ const handleView = useCallback((creation: Creation) => {
95
+ setSelectedCreation(creation);
96
+ }, []);
97
+
98
+ const styles = useStyles(tokens);
99
+
100
+ const renderEmptyComponent = useMemo(() => (
101
+ <GalleryEmptyStates
102
+ isLoading={isLoading}
103
+ creations={creations}
104
+ isFiltered={isFiltered}
105
+ tokens={tokens}
106
+ t={t}
107
+ emptyTitle={t(config.translations.empty)}
108
+ emptyDescription={t(config.translations.emptyDescription)}
109
+ emptyActionLabel={emptyActionLabel}
110
+ onEmptyAction={onEmptyAction}
111
+ onClearFilters={clearFilters}
112
+ />
113
+ ), [isLoading, creations, isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, clearFilters]);
114
+
115
+ if (selectedCreation) {
116
+ return (
117
+ <CreationDetailScreen
118
+ creation={selectedCreation}
119
+ onClose={() => setSelectedCreation(null)}
120
+ onShare={handleShare}
121
+ onDelete={handleDelete}
122
+ t={t}
123
+ />
124
+ );
125
+ }
126
+
127
+ const handleLayout = (event: LayoutChangeEvent) => {
128
+ // Keep internal logic if needed, currently empty but handles the event correctly
129
+ void event;
130
+ };
131
+
132
+ return (
133
+ <View style={styles.container} onLayout={handleLayout}>
134
+ {(!creations || creations?.length === 0) && !isLoading ? null : (
135
+ <GalleryHeader
136
+ title={t(config.translations.title) || 'My Creations'}
137
+ count={filtered.length}
138
+ countLabel={t(config.translations.photoCount) || 'photos'}
139
+ isFiltered={isFiltered}
140
+ filterLabel={t(config.translations.filterLabel) || 'Filter'}
141
+ onFilterPress={() => filterSheetRef.current?.present()}
142
+ style={{ paddingTop: insets.top + tokens.spacing.md }}
143
+ />
144
+ )}
145
+
146
+ {/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
147
+ <CreationsGrid
148
+ creations={filtered}
149
+ types={translatedTypes}
150
+ isLoading={isLoading}
151
+ onRefresh={() => void refetch()}
152
+ onView={handleView}
153
+ onShare={handleShare}
154
+ onDelete={handleDelete}
155
+ contentContainerStyle={{ paddingBottom: insets.bottom + tokens.spacing.xl }}
156
+ ListEmptyComponent={renderEmptyComponent}
157
+ />
158
+
159
+ <CreationImageViewer
160
+ creations={filtered}
161
+ visible={viewerVisible}
162
+ index={viewerIndex}
163
+ onDismiss={() => setViewerVisible(false)}
164
+ onIndexChange={setViewerIndex}
165
+ enableEditing={enableEditing}
166
+ onImageEdit={onImageEdit}
167
+ selectedCreationId={selectedCreation?.id}
168
+ />
169
+
170
+ <FilterBottomSheet
171
+ ref={filterSheetRef}
172
+ categories={allCategories}
173
+ selectedIds={selectedIds}
174
+ onFilterPress={(id, catId) => {
175
+ const category = allCategories.find(c => c.id === catId);
176
+ toggleFilter(id, category?.multiSelect);
177
+ }}
178
+ onClearFilters={clearFilters}
179
+ title={t(config.translations.filterTitle) || t("common.filter")}
180
+ />
181
+ </View>
182
+ );
183
+ }
184
+
185
+ const useStyles = (tokens: DesignTokens) => StyleSheet.create({
186
+ container: { flex: 1, backgroundColor: tokens.colors.background },
187
+ centerContainer: {
188
+ flex: 1,
189
+ justifyContent: 'center',
190
+ alignItems: 'center',
191
+ minHeight: 400,
192
+ paddingHorizontal: tokens.spacing.xl
193
+ },
194
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Presentation Screens
3
+ */
4
+
5
+ export { CreationsGalleryScreen } from "./CreationsGalleryScreen";
@@ -0,0 +1,52 @@
1
+ import { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
2
+ import { FilterCategory } from "../components/FilterBottomSheet";
3
+
4
+ /**
5
+ * Transforms the creations configuration into filter categories for the UI.
6
+ *
7
+ * @param config The creations configuration object
8
+ * @param t Translation function
9
+ * @returns Array of FilterCategory
10
+ */
11
+ export const getFilterCategoriesFromConfig = (
12
+ config: CreationsConfig,
13
+ t: (key: string) => string
14
+ ): FilterCategory[] => {
15
+ const categories: FilterCategory[] = [];
16
+
17
+ if (config.types.length > 0) {
18
+ categories.push({
19
+ id: 'type',
20
+ title: t(config.translations.filterTitle),
21
+ multiSelect: false,
22
+ options: config.types.map(type => ({
23
+ id: type.id,
24
+ label: t(type.labelKey),
25
+ icon: type.icon || 'image'
26
+ }))
27
+ });
28
+ }
29
+
30
+ if (config.filterCategories) {
31
+ categories.push(...config.filterCategories);
32
+ }
33
+
34
+ return categories;
35
+ };
36
+
37
+ /**
38
+ * Translates the creation types for display.
39
+ *
40
+ * @param config The creations configuration object
41
+ * @param t Translation function
42
+ * @returns Array of types with translated labels
43
+ */
44
+ export const getTranslatedTypes = (
45
+ config: CreationsConfig,
46
+ t: (key: string) => string
47
+ ) => {
48
+ return config.types.map(type => ({
49
+ ...type,
50
+ labelKey: t(type.labelKey)
51
+ }));
52
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Type declarations for external modules
3
+ */
4
+
5
+ declare module "@umituz/react-native-firestore" {
6
+ import type { Firestore } from "firebase/firestore";
7
+
8
+ export class BaseRepository {
9
+ protected getDb(): Firestore | null;
10
+ protected getDbOrThrow(): Firestore;
11
+ protected isDbInitialized(): boolean;
12
+ protected isQuotaError(error: unknown): boolean;
13
+ protected handleQuotaError(error: unknown): never;
14
+ protected executeWithQuotaHandling<T>(
15
+ operation: () => Promise<T>,
16
+ ): Promise<T>;
17
+ protected trackRead(
18
+ collection: string,
19
+ count: number,
20
+ cached: boolean,
21
+ ): void;
22
+ protected trackWrite(
23
+ collection: string,
24
+ docId: string,
25
+ count: number,
26
+ ): void;
27
+ protected trackDelete(
28
+ collection: string,
29
+ docId: string,
30
+ count: number,
31
+ ): void;
32
+ destroy(): void;
33
+ }
34
+
35
+ export function getFirestore(): Firestore | null;
36
+ export function initializeFirestore(app: unknown): void;
37
+ export function isFirestoreInitialized(): boolean;
38
+ }
39
+
40
+ declare module "@umituz/react-native-sentry";
41
+ declare module "expo-apple-authentication";
42
+ declare module "@umituz/react-native-filesystem";