@umituz/react-native-ai-generation-content 1.12.3 → 1.12.5
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 +30 -6
- package/src/domains/content-moderation/domain/entities/moderation.types.ts +84 -0
- package/src/domains/content-moderation/domain/interfaces/content-filter.interface.ts +24 -0
- package/src/domains/content-moderation/index.ts +67 -0
- package/src/domains/content-moderation/infrastructure/rules/default-rules.data.ts +144 -0
- package/src/domains/content-moderation/infrastructure/rules/rules-registry.ts +75 -0
- package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +150 -0
- package/src/domains/content-moderation/infrastructure/services/index.ts +8 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/base.moderator.ts +62 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/image.moderator.ts +64 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/index.ts +10 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +144 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/video.moderator.ts +64 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/voice.moderator.ts +74 -0
- package/src/domains/content-moderation/infrastructure/services/pattern-matcher.service.ts +51 -0
- package/src/domains/content-moderation/presentation/exceptions/content-policy-violation.exception.ts +48 -0
- package/src/domains/creations/application/services/CreationsService.ts +71 -0
- package/src/domains/creations/domain/entities/Creation.ts +51 -0
- package/src/domains/creations/domain/entities/index.ts +6 -0
- package/src/domains/creations/domain/repositories/ICreationsRepository.ts +23 -0
- package/src/domains/creations/domain/repositories/index.ts +5 -0
- package/src/domains/creations/domain/services/ICreationsStorageService.ts +13 -0
- package/src/domains/creations/domain/value-objects/CreationsConfig.ts +76 -0
- package/src/domains/creations/domain/value-objects/index.ts +12 -0
- package/src/domains/creations/index.ts +84 -0
- package/src/domains/creations/infrastructure/adapters/createRepository.ts +54 -0
- package/src/domains/creations/infrastructure/adapters/index.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +233 -0
- package/src/domains/creations/infrastructure/repositories/index.ts +8 -0
- package/src/domains/creations/infrastructure/services/CreationsStorageService.ts +48 -0
- package/src/domains/creations/presentation/components/CreationCard.tsx +136 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailActions.tsx +76 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailHeader.tsx +81 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +41 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailStory.tsx +67 -0
- package/src/domains/creations/presentation/components/CreationDetail/index.ts +4 -0
- package/src/domains/creations/presentation/components/CreationImageViewer.tsx +43 -0
- package/src/domains/creations/presentation/components/CreationThumbnail.tsx +63 -0
- package/src/domains/creations/presentation/components/CreationsGrid.tsx +75 -0
- package/src/domains/creations/presentation/components/CreationsHomeCard.tsx +176 -0
- package/src/domains/creations/presentation/components/EmptyState.tsx +75 -0
- package/src/domains/creations/presentation/components/FilterBottomSheet.tsx +158 -0
- package/src/domains/creations/presentation/components/FilterChips.tsx +105 -0
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +106 -0
- package/src/domains/creations/presentation/components/index.ts +19 -0
- package/src/domains/creations/presentation/hooks/index.ts +7 -0
- package/src/domains/creations/presentation/hooks/useCreations.ts +33 -0
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +70 -0
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +51 -0
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +71 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +217 -0
- package/src/domains/creations/presentation/screens/index.ts +5 -0
- package/src/domains/creations/presentation/utils/filterUtils.ts +52 -0
- package/src/domains/creations/types.d.ts +107 -0
- package/src/domains/face-detection/domain/constants/faceDetectionConstants.ts +16 -0
- package/src/domains/face-detection/domain/entities/FaceDetection.ts +19 -0
- package/src/domains/face-detection/index.ts +26 -0
- package/src/domains/face-detection/infrastructure/analyzers/faceAnalyzer.ts +36 -0
- package/src/domains/face-detection/infrastructure/validators/faceValidator.ts +52 -0
- package/src/domains/face-detection/presentation/components/FaceValidationStatus.tsx +111 -0
- package/src/domains/face-detection/presentation/hooks/useFaceDetection.ts +58 -0
- package/src/domains/feature-background/domain/entities/background.types.ts +77 -0
- package/src/domains/feature-background/domain/entities/component.types.ts +96 -0
- package/src/domains/feature-background/domain/entities/config.types.ts +41 -0
- package/src/domains/feature-background/domain/entities/index.ts +31 -0
- package/src/domains/feature-background/index.ts +72 -0
- package/src/domains/feature-background/infrastructure/constants/index.ts +5 -0
- package/src/domains/feature-background/infrastructure/constants/prompts.constants.ts +15 -0
- package/src/domains/feature-background/presentation/components/BackgroundFeature.tsx +145 -0
- package/src/domains/feature-background/presentation/components/ComparisonSlider.tsx +199 -0
- package/src/domains/feature-background/presentation/components/ErrorDisplay.tsx +58 -0
- package/src/domains/feature-background/presentation/components/FeatureHeader.tsx +80 -0
- package/src/domains/feature-background/presentation/components/GenerateButton.tsx +86 -0
- package/src/domains/feature-background/presentation/components/ImagePicker.tsx +136 -0
- package/src/domains/feature-background/presentation/components/ModeSelector.tsx +78 -0
- package/src/domains/feature-background/presentation/components/ProcessingModal.tsx +113 -0
- package/src/domains/feature-background/presentation/components/PromptInput.tsx +142 -0
- package/src/domains/feature-background/presentation/components/ResultDisplay.tsx +123 -0
- package/src/domains/feature-background/presentation/components/index.ts +16 -0
- package/src/domains/feature-background/presentation/hooks/index.ts +7 -0
- package/src/domains/feature-background/presentation/hooks/useBackgroundFeature.ts +118 -0
- package/src/index.ts +24 -0
|
@@ -0,0 +1,75 @@
|
|
|
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, AtomicButton, 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.headingSmall,
|
|
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
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
[tokens, onAction],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View style={styles.container}>
|
|
60
|
+
<View style={styles.iconContainer}>
|
|
61
|
+
<AtomicIcon name={iconName} size={64} color="secondary" />
|
|
62
|
+
</View>
|
|
63
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
64
|
+
<AtomicText style={styles.description}>{description}</AtomicText>
|
|
65
|
+
{onAction && actionLabel && (
|
|
66
|
+
<AtomicButton
|
|
67
|
+
title={actionLabel}
|
|
68
|
+
onPress={onAction}
|
|
69
|
+
variant="primary"
|
|
70
|
+
style={styles.button}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
|
3
|
+
import { BottomSheetModal, BottomSheetModalRef } from '@umituz/react-native-bottom-sheet';
|
|
4
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
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<BottomSheetModalRef, 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 as any}
|
|
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="check" 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
|
+
const useStyles = (tokens: any) => StyleSheet.create({
|
|
94
|
+
content: {
|
|
95
|
+
padding: tokens.spacing.md,
|
|
96
|
+
paddingBottom: tokens.spacing.xl,
|
|
97
|
+
},
|
|
98
|
+
header: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
justifyContent: 'space-between',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
paddingHorizontal: tokens.spacing.md,
|
|
103
|
+
paddingBottom: tokens.spacing.sm,
|
|
104
|
+
borderBottomWidth: 1,
|
|
105
|
+
borderBottomColor: tokens.colors.outline,
|
|
106
|
+
},
|
|
107
|
+
headerTitle: {
|
|
108
|
+
fontSize: 20,
|
|
109
|
+
fontWeight: '700',
|
|
110
|
+
color: tokens.colors.textPrimary,
|
|
111
|
+
},
|
|
112
|
+
clearButton: {
|
|
113
|
+
color: tokens.colors.primary,
|
|
114
|
+
fontSize: 14,
|
|
115
|
+
fontWeight: '600',
|
|
116
|
+
},
|
|
117
|
+
categoryContainer: {
|
|
118
|
+
marginTop: tokens.spacing.md,
|
|
119
|
+
},
|
|
120
|
+
categoryTitle: {
|
|
121
|
+
marginBottom: tokens.spacing.xs,
|
|
122
|
+
color: tokens.colors.textSecondary,
|
|
123
|
+
fontSize: 16,
|
|
124
|
+
fontWeight: '600',
|
|
125
|
+
},
|
|
126
|
+
optionsContainer: {
|
|
127
|
+
backgroundColor: tokens.colors.background,
|
|
128
|
+
borderRadius: tokens.borderRadius.md,
|
|
129
|
+
overflow: 'hidden',
|
|
130
|
+
},
|
|
131
|
+
option: {
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
justifyContent: 'space-between',
|
|
135
|
+
padding: tokens.spacing.md,
|
|
136
|
+
backgroundColor: tokens.colors.background,
|
|
137
|
+
borderBottomWidth: 1,
|
|
138
|
+
borderBottomColor: tokens.colors.surface,
|
|
139
|
+
},
|
|
140
|
+
optionSelected: {
|
|
141
|
+
backgroundColor: tokens.colors.surface,
|
|
142
|
+
},
|
|
143
|
+
optionContent: {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
},
|
|
147
|
+
optionIcon: {
|
|
148
|
+
marginRight: tokens.spacing.sm,
|
|
149
|
+
},
|
|
150
|
+
optionLabel: {
|
|
151
|
+
color: tokens.colors.text,
|
|
152
|
+
fontSize: 14,
|
|
153
|
+
},
|
|
154
|
+
optionLabelSelected: {
|
|
155
|
+
color: tokens.colors.primary,
|
|
156
|
+
fontWeight: 'bold',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -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 } 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?: any;
|
|
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.spacing.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 } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens } 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?: any;
|
|
13
|
+
readonly style?: any;
|
|
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: any) => 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,33 @@
|
|
|
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: () => repository.getAll(userId!),
|
|
29
|
+
enabled: !!userId && enabled,
|
|
30
|
+
staleTime: CACHE_CONFIG.staleTime,
|
|
31
|
+
gcTime: CACHE_CONFIG.gcTime,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
export function useCreationsFilter({
|
|
15
|
+
creations,
|
|
16
|
+
defaultFilterId = "all"
|
|
17
|
+
}: UseCreationsFilterProps) {
|
|
18
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
|
|
19
|
+
|
|
20
|
+
const filtered = useMemo(() => {
|
|
21
|
+
if (!creations) return [];
|
|
22
|
+
if (selectedIds.includes(defaultFilterId)) return creations;
|
|
23
|
+
|
|
24
|
+
return creations.filter((c) =>
|
|
25
|
+
selectedIds.includes(c.type) ||
|
|
26
|
+
selectedIds.some(id => (c as any).metadata?.tags?.includes(id))
|
|
27
|
+
);
|
|
28
|
+
}, [creations, selectedIds, defaultFilterId]);
|
|
29
|
+
|
|
30
|
+
const toggleFilter = useCallback((filterId: string, multiSelect: boolean = false) => {
|
|
31
|
+
setSelectedIds(prev => {
|
|
32
|
+
// If selecting 'all', clear everything else
|
|
33
|
+
if (filterId === defaultFilterId) return [defaultFilterId];
|
|
34
|
+
|
|
35
|
+
let newIds: string[];
|
|
36
|
+
if (!multiSelect) {
|
|
37
|
+
// Single select
|
|
38
|
+
// If we tap the already selected item in single mode, should we Deselect it?
|
|
39
|
+
// Typically in radio-button style filters, no. But let's assume valid toggling behavior suitable for the UI.
|
|
40
|
+
// If single select, simply switch to the new one.
|
|
41
|
+
if (prev.includes(filterId) && prev.length === 1) return prev;
|
|
42
|
+
newIds = [filterId];
|
|
43
|
+
} else {
|
|
44
|
+
// Multi select
|
|
45
|
+
if (prev.includes(filterId)) {
|
|
46
|
+
newIds = prev.filter(id => id !== filterId);
|
|
47
|
+
} else {
|
|
48
|
+
// Remove 'all' if present
|
|
49
|
+
newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If nothing selected, revert to 'all'
|
|
54
|
+
if (newIds.length === 0) return [defaultFilterId];
|
|
55
|
+
return newIds;
|
|
56
|
+
});
|
|
57
|
+
}, [defaultFilterId]);
|
|
58
|
+
|
|
59
|
+
const clearFilters = useCallback(() => {
|
|
60
|
+
setSelectedIds([defaultFilterId]);
|
|
61
|
+
}, [defaultFilterId]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
filtered,
|
|
65
|
+
selectedIds,
|
|
66
|
+
toggleFilter,
|
|
67
|
+
clearFilters,
|
|
68
|
+
isFiltered: !selectedIds.includes(defaultFilterId),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -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
|
+
}
|