@umituz/react-native-ai-generation-content 1.12.4 → 1.12.6
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 +25 -6
- 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 +40 -0
- package/src/domains/face-detection/infrastructure/validators/faceValidator.ts +52 -0
- package/src/domains/face-detection/presentation/components/FaceValidationStatus.tsx +119 -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 +18 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet, ScrollView } from 'react-native';
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
import type { Creation } from '../../domain/entities/Creation';
|
|
6
|
+
import { DetailHeader } from '../components/CreationDetail/DetailHeader';
|
|
7
|
+
import { DetailImage } from '../components/CreationDetail/DetailImage';
|
|
8
|
+
import { DetailStory } from '../components/CreationDetail/DetailStory';
|
|
9
|
+
import { DetailActions } from '../components/CreationDetail/DetailActions';
|
|
10
|
+
|
|
11
|
+
interface CreationDetailScreenProps {
|
|
12
|
+
readonly creation: Creation;
|
|
13
|
+
readonly onClose: () => void;
|
|
14
|
+
readonly onShare: (creation: Creation) => void;
|
|
15
|
+
readonly onDelete: (creation: Creation) => void;
|
|
16
|
+
readonly t: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
20
|
+
creation,
|
|
21
|
+
onClose,
|
|
22
|
+
onShare,
|
|
23
|
+
onDelete,
|
|
24
|
+
t
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
// Extract data
|
|
29
|
+
const metadata = (creation as any).metadata || {};
|
|
30
|
+
const title = metadata.names || creation.type;
|
|
31
|
+
const story = metadata.story || metadata.description || "";
|
|
32
|
+
const date = metadata.date || new Date(creation.createdAt).toLocaleDateString();
|
|
33
|
+
|
|
34
|
+
const styles = useStyles(tokens);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={styles.container}>
|
|
38
|
+
<DetailHeader
|
|
39
|
+
title={title}
|
|
40
|
+
date={date}
|
|
41
|
+
onClose={onClose}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<ScrollView
|
|
45
|
+
contentContainerStyle={styles.scrollContent}
|
|
46
|
+
showsVerticalScrollIndicator={false}
|
|
47
|
+
>
|
|
48
|
+
<DetailImage uri={creation.uri} />
|
|
49
|
+
|
|
50
|
+
<DetailStory story={story} />
|
|
51
|
+
|
|
52
|
+
<DetailActions
|
|
53
|
+
onShare={() => onShare(creation)}
|
|
54
|
+
onDelete={() => onDelete(creation)}
|
|
55
|
+
shareLabel={t("result.shareButton") || "Share"}
|
|
56
|
+
deleteLabel={t("common.delete") || "Delete"}
|
|
57
|
+
/>
|
|
58
|
+
</ScrollView>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const useStyles = (tokens: any) => StyleSheet.create({
|
|
64
|
+
container: {
|
|
65
|
+
flex: 1,
|
|
66
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
67
|
+
},
|
|
68
|
+
scrollContent: {
|
|
69
|
+
paddingBottom: tokens.spacing.xxl,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useMemo, useCallback, useState } from "react";
|
|
2
|
+
import { View, StyleSheet, ActivityIndicator } from "react-native";
|
|
3
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
import { useSharing } from "@umituz/react-native-sharing";
|
|
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 { useAlert } from "@umituz/react-native-alert";
|
|
11
|
+
import { BottomSheetModalRef } from "@umituz/react-native-bottom-sheet";
|
|
12
|
+
import { GalleryHeader, EmptyState, CreationsGrid, FilterBottomSheet, CreationImageViewer, type FilterCategory } from "../components";
|
|
13
|
+
import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
|
|
14
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
15
|
+
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
16
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
17
|
+
import { CreationDetailScreen } from "./CreationDetailScreen";
|
|
18
|
+
|
|
19
|
+
interface CreationsGalleryScreenProps {
|
|
20
|
+
readonly userId: string | null;
|
|
21
|
+
readonly repository: ICreationsRepository;
|
|
22
|
+
readonly config: CreationsConfig;
|
|
23
|
+
readonly t: (key: string) => string;
|
|
24
|
+
readonly enableEditing?: boolean;
|
|
25
|
+
readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
|
|
26
|
+
readonly onEmptyAction?: () => void;
|
|
27
|
+
readonly emptyActionLabel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CreationsGalleryScreen({
|
|
31
|
+
userId,
|
|
32
|
+
repository,
|
|
33
|
+
config,
|
|
34
|
+
t,
|
|
35
|
+
enableEditing = false,
|
|
36
|
+
onImageEdit,
|
|
37
|
+
onEmptyAction,
|
|
38
|
+
emptyActionLabel,
|
|
39
|
+
}: CreationsGalleryScreenProps) {
|
|
40
|
+
const tokens = useAppDesignTokens();
|
|
41
|
+
const insets = useSafeAreaInsets();
|
|
42
|
+
const { share } = useSharing();
|
|
43
|
+
const alert = useAlert();
|
|
44
|
+
|
|
45
|
+
const [viewerVisible, setViewerVisible] = useState(false);
|
|
46
|
+
const [viewerIndex, setViewerIndex] = useState(0);
|
|
47
|
+
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
48
|
+
const filterSheetRef = React.useRef<BottomSheetModalRef>(null);
|
|
49
|
+
|
|
50
|
+
const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
|
|
51
|
+
const creations = creationsData as Creation[] | undefined;
|
|
52
|
+
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
53
|
+
const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
|
|
54
|
+
|
|
55
|
+
// Refetch creations when screen comes into focus
|
|
56
|
+
useFocusEffect(
|
|
57
|
+
useCallback(() => {
|
|
58
|
+
refetch();
|
|
59
|
+
}, [refetch])
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Prepare data for UI using utils
|
|
63
|
+
const translatedTypes = useMemo(() => getTranslatedTypes(config, t), [config, t]);
|
|
64
|
+
const allCategories = useMemo(() => getFilterCategoriesFromConfig(config, t), [config, t]);
|
|
65
|
+
|
|
66
|
+
const handleShare = useCallback(async (creation: Creation) => {
|
|
67
|
+
share(creation.uri, { dialogTitle: t("common.share") });
|
|
68
|
+
}, [share, t]);
|
|
69
|
+
|
|
70
|
+
const handleDelete = useCallback(async (creation: Creation) => {
|
|
71
|
+
alert.show({
|
|
72
|
+
title: t(config.translations.deleteTitle),
|
|
73
|
+
message: t(config.translations.deleteMessage),
|
|
74
|
+
type: 'warning' as any,
|
|
75
|
+
actions: [
|
|
76
|
+
{ id: 'cancel', label: t("common.cancel"), onPress: () => { } },
|
|
77
|
+
{
|
|
78
|
+
id: 'delete', label: t("common.delete"), variant: 'danger' as any, onPress: async () => {
|
|
79
|
+
const success = await deleteMutation.mutateAsync(creation.id);
|
|
80
|
+
if (success) setSelectedCreation(null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}, [alert, config, deleteMutation, t]);
|
|
86
|
+
|
|
87
|
+
// Handle viewing a creation - shows detail screen
|
|
88
|
+
const handleView = useCallback((creation: Creation) => {
|
|
89
|
+
setSelectedCreation(creation);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const styles = useStyles(tokens);
|
|
93
|
+
|
|
94
|
+
// Define empty state content based on state
|
|
95
|
+
const renderEmptyComponent = useMemo(() => {
|
|
96
|
+
// 1. Loading State
|
|
97
|
+
if (isLoading && (!creations || creations?.length === 0)) {
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.centerContainer}>
|
|
100
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. System Empty State (User has NO creations at all)
|
|
106
|
+
// We check 'creations' (the full list)
|
|
107
|
+
if (!creations || creations?.length === 0) {
|
|
108
|
+
return (
|
|
109
|
+
<View style={styles.centerContainer}>
|
|
110
|
+
<EmptyState
|
|
111
|
+
title={t(config.translations.empty)}
|
|
112
|
+
description={t(config.translations.emptyDescription)}
|
|
113
|
+
actionLabel={emptyActionLabel}
|
|
114
|
+
onAction={onEmptyAction}
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Filter Empty State (User has creations, but filter returns none)
|
|
121
|
+
// We check 'filtered' (the displayed list)
|
|
122
|
+
return (
|
|
123
|
+
<View style={styles.centerContainer}>
|
|
124
|
+
<EmptyState
|
|
125
|
+
title={t("common.no_results") || "No results"}
|
|
126
|
+
description={t("common.no_results_description") || "Try changing your filters"}
|
|
127
|
+
actionLabel={t("common.clear_all") || "Clear All"}
|
|
128
|
+
onAction={clearFilters}
|
|
129
|
+
/>
|
|
130
|
+
</View>
|
|
131
|
+
);
|
|
132
|
+
}, [isLoading, creations, config, t, emptyActionLabel, onEmptyAction, clearFilters, styles.centerContainer, tokens.colors.primary]);
|
|
133
|
+
|
|
134
|
+
if (selectedCreation) {
|
|
135
|
+
return (
|
|
136
|
+
<CreationDetailScreen
|
|
137
|
+
creation={selectedCreation}
|
|
138
|
+
onClose={() => setSelectedCreation(null)}
|
|
139
|
+
onShare={handleShare}
|
|
140
|
+
onDelete={handleDelete}
|
|
141
|
+
t={t}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View style={styles.container}>
|
|
148
|
+
{/* Header is always shown unless we are in "System Empty" without data?
|
|
149
|
+
User requested: "herhangi bir creations yoksa buradaki no creations gözükmeli" (if no creations, show no creations).
|
|
150
|
+
Currently we show header always, except logic below might hide it.
|
|
151
|
+
Actually, let's keep header always visible IF we have creations.
|
|
152
|
+
If !creations, we pass `renderEmptyComponent`, but Grid has header support.
|
|
153
|
+
|
|
154
|
+
However, to match previous request "filter gözükebilir" (filter can be visible), we'll keep header outside.
|
|
155
|
+
BUT, if NO creations, showing filter header is weird.
|
|
156
|
+
|
|
157
|
+
Let's conditonally render header: Only if we have creations OR loading.
|
|
158
|
+
If loaded and 0 creations -> Hide header (Clean Empty State).
|
|
159
|
+
*/}
|
|
160
|
+
{(!creations || creations?.length === 0) && !isLoading ? null : (
|
|
161
|
+
<GalleryHeader
|
|
162
|
+
title={t(config.translations.title) || 'My Creations'}
|
|
163
|
+
count={filtered.length}
|
|
164
|
+
countLabel={t(config.translations.photoCount) || 'photos'}
|
|
165
|
+
isFiltered={isFiltered}
|
|
166
|
+
filterLabel={t(config.translations.filterLabel) || 'Filter'}
|
|
167
|
+
onFilterPress={() => filterSheetRef.current?.present()}
|
|
168
|
+
style={{ paddingTop: insets.top + tokens.spacing.md }}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
|
|
173
|
+
<CreationsGrid
|
|
174
|
+
creations={filtered}
|
|
175
|
+
types={translatedTypes}
|
|
176
|
+
isLoading={isLoading}
|
|
177
|
+
onRefresh={refetch}
|
|
178
|
+
onView={handleView}
|
|
179
|
+
onShare={handleShare}
|
|
180
|
+
onDelete={handleDelete}
|
|
181
|
+
contentContainerStyle={{ paddingBottom: insets.bottom + tokens.spacing.xl }}
|
|
182
|
+
ListEmptyComponent={renderEmptyComponent}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<CreationImageViewer
|
|
186
|
+
creations={filtered}
|
|
187
|
+
visible={viewerVisible}
|
|
188
|
+
index={viewerIndex}
|
|
189
|
+
onDismiss={() => setViewerVisible(false)}
|
|
190
|
+
onIndexChange={setViewerIndex}
|
|
191
|
+
enableEditing={enableEditing}
|
|
192
|
+
onImageEdit={onImageEdit}
|
|
193
|
+
selectedCreationId={(selectedCreation as Creation | null)?.id}
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
<FilterBottomSheet
|
|
197
|
+
ref={filterSheetRef}
|
|
198
|
+
categories={allCategories}
|
|
199
|
+
selectedIds={selectedIds}
|
|
200
|
+
onFilterPress={(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
|
|
201
|
+
onClearFilters={clearFilters}
|
|
202
|
+
title={t(config.translations.filterTitle) || t("common.filter")}
|
|
203
|
+
/>
|
|
204
|
+
</View>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const useStyles = (tokens: any) => StyleSheet.create({
|
|
209
|
+
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
210
|
+
centerContainer: {
|
|
211
|
+
flex: 1,
|
|
212
|
+
justifyContent: 'center',
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
minHeight: 400,
|
|
215
|
+
paddingHorizontal: tokens.spacing.xl
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -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
|
+
};
|