@umituz/react-native-ai-generation-content 1.12.24 → 1.12.26

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 (53) hide show
  1. package/package.json +2 -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/GalleryHeader.tsx +106 -0
  30. package/src/domains/creations/presentation/components/index.ts +19 -0
  31. package/src/domains/creations/presentation/hooks/index.ts +7 -0
  32. package/src/domains/creations/presentation/hooks/useCreations.ts +38 -0
  33. package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +77 -0
  34. package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +51 -0
  35. package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +78 -0
  36. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +217 -0
  37. package/src/domains/creations/presentation/screens/index.ts +5 -0
  38. package/src/domains/creations/presentation/utils/filterUtils.ts +52 -0
  39. package/src/domains/creations/types.d.ts +42 -0
  40. package/src/features/background/presentation/components/ComparisonSlider.tsx +4 -4
  41. package/src/features/background/presentation/components/ErrorDisplay.tsx +3 -3
  42. package/src/features/background/presentation/components/FeatureHeader.tsx +1 -1
  43. package/src/features/background/presentation/components/GenerateButton.tsx +0 -2
  44. package/src/features/background/presentation/components/ImagePicker.tsx +3 -3
  45. package/src/features/background/presentation/components/ProcessingModal.tsx +2 -2
  46. package/src/features/background/presentation/components/PromptInput.tsx +5 -5
  47. package/src/features/background/presentation/components/ResultDisplay.tsx +4 -4
  48. package/src/index.ts +5 -0
  49. package/src/presentation/components/GenerationProgressContent.tsx +4 -4
  50. package/src/presentation/components/PendingJobCard.tsx +2 -2
  51. package/src/presentation/components/PendingJobCardActions.tsx +2 -2
  52. package/src/presentation/components/result/GenerationResultContent.tsx +2 -3
  53. package/src/presentation/hooks/usePhotoGeneration.ts +7 -5
@@ -0,0 +1,176 @@
1
+ /**
2
+ * CreationsHomeCard Component
3
+ * Shows user's creations preview on home screen
4
+ */
5
+
6
+ import React, { useMemo, useCallback } from "react";
7
+ import { View, TouchableOpacity, FlatList, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { Creation } from "../../domain/entities/Creation";
14
+ import { CreationThumbnail } from "./CreationThumbnail";
15
+
16
+ interface CreationsHomeCardProps {
17
+ readonly creations: Creation[] | undefined;
18
+ readonly isLoading: boolean;
19
+ readonly title: string;
20
+ readonly countLabel: string;
21
+ readonly loadingLabel: string;
22
+ readonly maxThumbnails?: number;
23
+ readonly onPress: () => void;
24
+ }
25
+
26
+ export function CreationsHomeCard({
27
+ creations,
28
+ isLoading,
29
+ title,
30
+ countLabel,
31
+ loadingLabel,
32
+ maxThumbnails = 4,
33
+ onPress,
34
+ }: CreationsHomeCardProps) {
35
+ const tokens = useAppDesignTokens();
36
+
37
+ // eslint-disable-next-line no-console
38
+ if (__DEV__) {
39
+ // eslint-disable-next-line no-console
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.borders.radius.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.borders.radius.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,82 @@
1
+ /**
2
+ * EmptyState Component
3
+ * Displays when no creations exist
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
9
+
10
+ interface EmptyStateProps {
11
+ readonly title: string;
12
+ readonly description: string;
13
+ readonly iconName?: string;
14
+ readonly actionLabel?: string;
15
+ readonly onAction?: () => void;
16
+ }
17
+
18
+ export function EmptyState({
19
+ title,
20
+ description,
21
+ iconName = "images-outline",
22
+ actionLabel,
23
+ onAction,
24
+ }: EmptyStateProps) {
25
+ const tokens = useAppDesignTokens();
26
+
27
+ const styles = useMemo(
28
+ () =>
29
+ StyleSheet.create({
30
+ container: {
31
+ flex: 1,
32
+ justifyContent: "center",
33
+ alignItems: "center",
34
+ padding: tokens.spacing.xl,
35
+ },
36
+ iconContainer: {
37
+ marginBottom: tokens.spacing.lg,
38
+ },
39
+ title: {
40
+ ...tokens.typography.headlineSmall,
41
+ color: tokens.colors.textPrimary,
42
+ textAlign: "center",
43
+ marginBottom: tokens.spacing.sm,
44
+ },
45
+ description: {
46
+ ...tokens.typography.bodyMedium,
47
+ color: tokens.colors.textSecondary,
48
+ textAlign: "center",
49
+ marginBottom: onAction ? tokens.spacing.xl : 0,
50
+ },
51
+ button: {
52
+ minWidth: 160,
53
+ backgroundColor: tokens.colors.primary,
54
+ paddingVertical: tokens.spacing.md,
55
+ paddingHorizontal: tokens.spacing.lg,
56
+ borderRadius: tokens.borders.radius.md,
57
+ alignItems: "center",
58
+ },
59
+ buttonText: {
60
+ ...tokens.typography.labelMedium,
61
+ color: tokens.colors.textInverse,
62
+ fontWeight: "600",
63
+ },
64
+ }),
65
+ [tokens, onAction],
66
+ );
67
+
68
+ return (
69
+ <View style={styles.container}>
70
+ <View style={styles.iconContainer}>
71
+ <AtomicIcon name={iconName} size="xl" color="secondary" />
72
+ </View>
73
+ <AtomicText style={styles.title}>{title}</AtomicText>
74
+ <AtomicText style={styles.description}>{description}</AtomicText>
75
+ {onAction && actionLabel && (
76
+ <TouchableOpacity onPress={onAction} style={styles.button}>
77
+ <AtomicText style={styles.buttonText}>{actionLabel}</AtomicText>
78
+ </TouchableOpacity>
79
+ )}
80
+ </View>
81
+ );
82
+ }
@@ -0,0 +1,160 @@
1
+ import React, { forwardRef, useCallback, useMemo } from 'react';
2
+ import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
3
+ import { useAppDesignTokens, AtomicText, AtomicIcon, type DesignTokens } from '@umituz/react-native-design-system';
4
+ import { BottomSheetModal } from '@gorhom/bottom-sheet';
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
+ snapPoints?: string[];
26
+ }
27
+
28
+ export const FilterBottomSheet = forwardRef<BottomSheetModal, FilterBottomSheetProps>((props, ref) => {
29
+ const { categories, selectedIds, onFilterPress, onClearFilters, title, snapPoints: propSnapPoints } = props;
30
+ const tokens = useAppDesignTokens();
31
+ const styles = useStyles(tokens);
32
+
33
+ const snapPoints = useMemo(() => propSnapPoints || ['50%', '75%'], [propSnapPoints]);
34
+
35
+ const renderOption = useCallback((option: FilterOption, category: FilterCategory) => {
36
+ const isSelected = selectedIds.includes(option.id);
37
+
38
+ return (
39
+ <TouchableOpacity
40
+ key={option.id}
41
+ style={[styles.option, isSelected && styles.optionSelected]}
42
+ onPress={() => onFilterPress(option.id, category.id)}
43
+ >
44
+ <View style={styles.optionContent}>
45
+ {option.icon && (
46
+ <View style={styles.optionIcon}>
47
+ <AtomicIcon
48
+ name={option.icon}
49
+ size="sm"
50
+ color={isSelected ? "primary" : "onSurface"}
51
+ />
52
+ </View>
53
+ )}
54
+ <AtomicText
55
+ style={[styles.optionLabel, isSelected && styles.optionLabelSelected]}
56
+ >
57
+ {option.label}
58
+ </AtomicText>
59
+ </View>
60
+ {isSelected && (
61
+ <AtomicIcon name="checkmark" size="sm" color="primary" />
62
+ )}
63
+ </TouchableOpacity>
64
+ );
65
+ }, [onFilterPress, selectedIds, styles]);
66
+
67
+ return (
68
+ <BottomSheetModal
69
+ ref={ref}
70
+ snapPoints={snapPoints}
71
+ >
72
+ <View style={styles.header}>
73
+ <AtomicText style={styles.headerTitle}>{title}</AtomicText>
74
+ <TouchableOpacity onPress={onClearFilters}>
75
+ <AtomicText style={styles.clearButton}>Clear</AtomicText>
76
+ </TouchableOpacity>
77
+ </View>
78
+
79
+ <ScrollView contentContainerStyle={styles.content}>
80
+ {categories.map(category => (
81
+ <View key={category.id} style={styles.categoryContainer}>
82
+ <AtomicText style={styles.categoryTitle}>{category.title}</AtomicText>
83
+ <View style={styles.optionsContainer}>
84
+ {category.options.map(option => renderOption(option, category))}
85
+ </View>
86
+ </View>
87
+ ))}
88
+ </ScrollView>
89
+ </BottomSheetModal>
90
+ );
91
+ });
92
+
93
+ FilterBottomSheet.displayName = 'FilterBottomSheet';
94
+
95
+ const useStyles = (tokens: DesignTokens) => StyleSheet.create({
96
+ content: {
97
+ padding: tokens.spacing.md,
98
+ paddingBottom: tokens.spacing.xl,
99
+ },
100
+ header: {
101
+ flexDirection: 'row',
102
+ justifyContent: 'space-between',
103
+ alignItems: 'center',
104
+ paddingHorizontal: tokens.spacing.md,
105
+ paddingBottom: tokens.spacing.sm,
106
+ borderBottomWidth: 1,
107
+ borderBottomColor: tokens.colors.outline,
108
+ },
109
+ headerTitle: {
110
+ fontSize: 20,
111
+ fontWeight: '700',
112
+ color: tokens.colors.textPrimary,
113
+ },
114
+ clearButton: {
115
+ color: tokens.colors.primary,
116
+ fontSize: 14,
117
+ fontWeight: '600',
118
+ },
119
+ categoryContainer: {
120
+ marginTop: tokens.spacing.md,
121
+ },
122
+ categoryTitle: {
123
+ marginBottom: tokens.spacing.xs,
124
+ color: tokens.colors.textSecondary,
125
+ fontSize: 16,
126
+ fontWeight: '600',
127
+ },
128
+ optionsContainer: {
129
+ backgroundColor: tokens.colors.background,
130
+ borderRadius: tokens.borders.radius.md,
131
+ overflow: 'hidden',
132
+ },
133
+ option: {
134
+ flexDirection: 'row',
135
+ alignItems: 'center',
136
+ justifyContent: 'space-between',
137
+ padding: tokens.spacing.md,
138
+ backgroundColor: tokens.colors.background,
139
+ borderBottomWidth: 1,
140
+ borderBottomColor: tokens.colors.surface,
141
+ },
142
+ optionSelected: {
143
+ backgroundColor: tokens.colors.surface,
144
+ },
145
+ optionContent: {
146
+ flexDirection: 'row',
147
+ alignItems: 'center',
148
+ },
149
+ optionIcon: {
150
+ marginRight: tokens.spacing.sm,
151
+ },
152
+ optionLabel: {
153
+ color: tokens.colors.textPrimary,
154
+ fontSize: 14,
155
+ },
156
+ optionLabelSelected: {
157
+ color: tokens.colors.primary,
158
+ fontWeight: 'bold',
159
+ },
160
+ });
@@ -0,0 +1,105 @@
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, type ViewStyle } 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
+ readonly style?: ViewStyle;
18
+ }
19
+
20
+ export function FilterChips({
21
+ types,
22
+ availableTypes,
23
+ selectedType,
24
+ allLabel,
25
+ onSelect,
26
+ style,
27
+ }: FilterChipsProps) {
28
+ const tokens = useAppDesignTokens();
29
+
30
+ const styles = useMemo(
31
+ () =>
32
+ StyleSheet.create({
33
+ container: {
34
+ marginBottom: tokens.spacing.md,
35
+ },
36
+ scrollContent: {
37
+ paddingHorizontal: tokens.spacing.md,
38
+ gap: tokens.spacing.sm,
39
+ flexDirection: "row",
40
+ },
41
+ chip: {
42
+ paddingHorizontal: tokens.spacing.md,
43
+ paddingVertical: tokens.spacing.sm,
44
+ borderRadius: tokens.borders.radius.lg,
45
+ backgroundColor: tokens.colors.backgroundSecondary,
46
+ },
47
+ chipSelected: {
48
+ backgroundColor: tokens.colors.primary,
49
+ },
50
+ chipText: {
51
+ ...tokens.typography.bodySmall,
52
+ color: tokens.colors.textSecondary,
53
+ },
54
+ chipTextSelected: {
55
+ color: tokens.colors.textInverse,
56
+ },
57
+ }),
58
+ [tokens],
59
+ );
60
+
61
+ const visibleTypes = types.filter((t) => availableTypes.includes(t.id));
62
+
63
+ return (
64
+ <View style={[styles.container, style]}>
65
+ <ScrollView
66
+ horizontal
67
+ showsHorizontalScrollIndicator={false}
68
+ contentContainerStyle={styles.scrollContent}
69
+ >
70
+ <TouchableOpacity
71
+ style={[styles.chip, selectedType === "all" && styles.chipSelected]}
72
+ onPress={() => onSelect("all")}
73
+ >
74
+ <AtomicText
75
+ style={[
76
+ styles.chipText,
77
+ selectedType === "all" && styles.chipTextSelected,
78
+ ]}
79
+ >
80
+ {allLabel}
81
+ </AtomicText>
82
+ </TouchableOpacity>
83
+ {visibleTypes.map((type) => (
84
+ <TouchableOpacity
85
+ key={type.id}
86
+ style={[
87
+ styles.chip,
88
+ selectedType === type.id && styles.chipSelected,
89
+ ]}
90
+ onPress={() => onSelect(type.id)}
91
+ >
92
+ <AtomicText
93
+ style={[
94
+ styles.chipText,
95
+ selectedType === type.id && styles.chipTextSelected,
96
+ ]}
97
+ >
98
+ {type.icon} {type.labelKey}
99
+ </AtomicText>
100
+ </TouchableOpacity>
101
+ ))}
102
+ </ScrollView>
103
+ </View>
104
+ );
105
+ }
@@ -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,19 @@
1
+ /**
2
+ * Presentation Components
3
+ */
4
+
5
+ export { GalleryHeader } from "./GalleryHeader";
6
+ export { EmptyState } from "./EmptyState";
7
+ export { FilterChips } from "./FilterChips";
8
+ export { CreationsHomeCard } from "./CreationsHomeCard";
9
+ export { CreationCard } from "./CreationCard";
10
+ export { CreationThumbnail } from "./CreationThumbnail";
11
+ export { CreationImageViewer } from "./CreationImageViewer";
12
+ export { CreationsGrid } from "./CreationsGrid";
13
+ export { FilterBottomSheet, type FilterCategory, type FilterOption } from "./FilterBottomSheet";
14
+
15
+ // Detail Components
16
+ export { DetailHeader } from "./CreationDetail/DetailHeader";
17
+ export { DetailImage } from "./CreationDetail/DetailImage";
18
+ export { DetailStory } from "./CreationDetail/DetailStory";
19
+ 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
+ }