@umituz/react-native-ai-generation-content 1.17.21 → 1.17.22
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 +1 -1
- package/src/domains/creations/domain/types/creation-filter.ts +1 -3
- package/src/domains/creations/domain/value-objects/CreationsConfig.ts +12 -0
- package/src/domains/creations/presentation/components/FilterSheets.tsx +63 -0
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +95 -93
- package/src/domains/creations/presentation/components/index.ts +1 -0
- package/src/domains/creations/presentation/hooks/index.ts +3 -0
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +35 -48
- package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +78 -0
- package/src/domains/creations/presentation/hooks/useMediaFilter.ts +54 -0
- package/src/domains/creations/presentation/hooks/useStatusFilter.ts +54 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +81 -156
- package/src/features/image-to-video/index.ts +90 -3
- package/src/features/image-to-video/presentation/components/AnimationStyleSelector.tsx +135 -0
- package/src/features/image-to-video/presentation/components/DurationSelector.tsx +110 -0
- package/src/features/image-to-video/presentation/components/GenerateButton.tsx +95 -0
- package/src/features/image-to-video/presentation/components/HeroSection.tsx +89 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.tsx +234 -0
- package/src/features/image-to-video/presentation/components/MusicMoodSelector.tsx +181 -0
- package/src/features/image-to-video/presentation/components/index.ts +30 -0
- package/src/features/image-to-video/presentation/hooks/index.ts +22 -0
- package/src/features/image-to-video/presentation/hooks/useFormState.ts +116 -0
- package/src/features/image-to-video/presentation/hooks/useGeneration.ts +85 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +93 -0
- package/src/features/image-to-video/presentation/index.ts +4 -0
- package/src/features/text-to-video/domain/types/callback.types.ts +50 -0
- package/src/features/text-to-video/domain/types/component.types.ts +106 -0
- package/src/features/text-to-video/domain/types/config.types.ts +61 -0
- package/src/features/text-to-video/domain/types/index.ts +48 -4
- package/src/features/text-to-video/domain/types/request.types.ts +36 -0
- package/src/features/text-to-video/domain/types/state.types.ts +53 -0
- package/src/features/text-to-video/index.ts +41 -3
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +1 -1
- package/src/features/text-to-video/presentation/components/FrameSelector.tsx +153 -0
- package/src/features/text-to-video/presentation/components/GenerationTabs.tsx +73 -0
- package/src/features/text-to-video/presentation/components/HeroSection.tsx +67 -0
- package/src/features/text-to-video/presentation/components/HintCarousel.tsx +96 -0
- package/src/features/text-to-video/presentation/components/OptionsPanel.tsx +123 -0
- package/src/features/text-to-video/presentation/components/index.ts +10 -0
- package/src/features/text-to-video/presentation/hooks/index.ts +11 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +77 -20
- package/src/features/text-to-video/presentation/hooks/useTextToVideoForm.ts +134 -0
- package/src/features/text-to-video/presentation/index.ts +6 -0
- package/src/features/text-to-video/domain/types/text-to-video.types.ts +0 -65
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CreationTypeId, CreationStatus, CreationCategory } from "./creation-types";
|
|
7
|
+
import { IMAGE_CREATION_TYPES, VIDEO_CREATION_TYPES, VOICE_CREATION_TYPES } from "./creation-categories";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Filter options for querying creations
|
|
@@ -114,9 +115,6 @@ export function calculateCreationStats(
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
// Calculate category counts from type counts
|
|
117
|
-
const { IMAGE_CREATION_TYPES, VIDEO_CREATION_TYPES, VOICE_CREATION_TYPES } =
|
|
118
|
-
require("./creation-categories");
|
|
119
|
-
|
|
120
118
|
for (const [typeId, count] of Object.entries(stats.byType)) {
|
|
121
119
|
if (IMAGE_CREATION_TYPES.includes(typeId)) {
|
|
122
120
|
stats.byCategory.image += count as number;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Creation, CreationDocument } from "../entities/Creation";
|
|
7
7
|
import type { FilterCategory } from "@umituz/react-native-design-system";
|
|
8
|
+
import type { FilterOption } from "../types/creation-filter";
|
|
8
9
|
|
|
9
10
|
export interface CreationType {
|
|
10
11
|
readonly id: string;
|
|
@@ -23,6 +24,16 @@ export interface CreationsTranslations {
|
|
|
23
24
|
readonly filterAll: string;
|
|
24
25
|
readonly filterLabel: string;
|
|
25
26
|
readonly filterTitle: string;
|
|
27
|
+
readonly statusFilterTitle?: string;
|
|
28
|
+
readonly mediaFilterTitle?: string;
|
|
29
|
+
readonly clearFilter?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreationsFilterConfig {
|
|
33
|
+
readonly statusOptions?: FilterOption[];
|
|
34
|
+
readonly mediaOptions?: FilterOption[];
|
|
35
|
+
readonly showStatusFilter?: boolean;
|
|
36
|
+
readonly showMediaFilter?: boolean;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/**
|
|
@@ -35,6 +46,7 @@ export interface CreationsConfig {
|
|
|
35
46
|
readonly collectionName: string;
|
|
36
47
|
readonly types: readonly CreationType[];
|
|
37
48
|
readonly filterCategories?: readonly FilterCategory[];
|
|
49
|
+
readonly filterConfig?: CreationsFilterConfig;
|
|
38
50
|
readonly translations: CreationsTranslations;
|
|
39
51
|
readonly showFilter?: boolean;
|
|
40
52
|
readonly maxThumbnails?: number;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Sheets Components
|
|
3
|
+
* Modal-based filter sheets for status and media type filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { FilterSheet } from "@umituz/react-native-design-system";
|
|
8
|
+
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
9
|
+
|
|
10
|
+
interface FilterSheetConfig {
|
|
11
|
+
readonly visible: boolean;
|
|
12
|
+
readonly onClose: () => void;
|
|
13
|
+
readonly options: FilterOption[];
|
|
14
|
+
readonly selectedId: string;
|
|
15
|
+
readonly onSelect: (id: string) => void;
|
|
16
|
+
readonly onClear: () => void;
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly clearLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const StatusFilterSheet: React.FC<FilterSheetConfig> = ({
|
|
22
|
+
visible,
|
|
23
|
+
onClose,
|
|
24
|
+
options,
|
|
25
|
+
selectedId,
|
|
26
|
+
onSelect,
|
|
27
|
+
onClear,
|
|
28
|
+
title,
|
|
29
|
+
clearLabel
|
|
30
|
+
}) => (
|
|
31
|
+
<FilterSheet
|
|
32
|
+
visible={visible}
|
|
33
|
+
onClose={onClose}
|
|
34
|
+
options={options}
|
|
35
|
+
selectedIds={[selectedId]}
|
|
36
|
+
onFilterPress={onSelect}
|
|
37
|
+
onClearFilters={onClear}
|
|
38
|
+
title={title}
|
|
39
|
+
clearLabel={clearLabel}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const MediaFilterSheet: React.FC<FilterSheetConfig> = ({
|
|
44
|
+
visible,
|
|
45
|
+
onClose,
|
|
46
|
+
options,
|
|
47
|
+
selectedId,
|
|
48
|
+
onSelect,
|
|
49
|
+
onClear,
|
|
50
|
+
title,
|
|
51
|
+
clearLabel
|
|
52
|
+
}) => (
|
|
53
|
+
<FilterSheet
|
|
54
|
+
visible={visible}
|
|
55
|
+
onClose={onClose}
|
|
56
|
+
options={options}
|
|
57
|
+
selectedIds={[selectedId]}
|
|
58
|
+
onFilterPress={onSelect}
|
|
59
|
+
onClearFilters={onClear}
|
|
60
|
+
title={title}
|
|
61
|
+
clearLabel={clearLabel}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
@@ -1,118 +1,120 @@
|
|
|
1
1
|
declare const __DEV__: boolean;
|
|
2
2
|
|
|
3
|
-
import React from
|
|
4
|
-
import { View, TouchableOpacity, StyleSheet, type ViewStyle } from
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { View, TouchableOpacity, StyleSheet, type ViewStyle } from "react-native";
|
|
5
5
|
import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
6
6
|
|
|
7
|
+
interface FilterButtonConfig {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly icon: string;
|
|
11
|
+
readonly isActive: boolean;
|
|
12
|
+
readonly onPress: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
interface GalleryHeaderProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
readonly filterLabel?: string;
|
|
15
|
-
readonly filterIcon?: string;
|
|
16
|
-
readonly style?: ViewStyle;
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly count: number;
|
|
18
|
+
readonly countLabel: string;
|
|
19
|
+
readonly filterButtons?: FilterButtonConfig[];
|
|
20
|
+
readonly showFilter?: boolean;
|
|
21
|
+
readonly style?: ViewStyle;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
filterLabel = 'Filter',
|
|
27
|
-
filterIcon = 'filter-outline',
|
|
28
|
-
style,
|
|
25
|
+
title,
|
|
26
|
+
count,
|
|
27
|
+
countLabel,
|
|
28
|
+
filterButtons = [],
|
|
29
|
+
showFilter = true,
|
|
30
|
+
style,
|
|
29
31
|
}) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
const styles = useStyles(tokens);
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
35
|
+
return (
|
|
36
|
+
<View style={[styles.headerArea, style]}>
|
|
37
|
+
<View>
|
|
38
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
39
|
+
<AtomicText style={styles.subtitle}>
|
|
40
|
+
{count} {countLabel}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
</View>
|
|
43
|
+
{showFilter && filterButtons.length > 0 && (
|
|
44
|
+
<View style={styles.filterRow}>
|
|
45
|
+
{filterButtons.map((btn) => (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
key={btn.id}
|
|
48
|
+
onPress={() => {
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(`[GalleryHeader] ${btn.id} filter pressed`);
|
|
52
|
+
}
|
|
53
|
+
btn.onPress();
|
|
54
|
+
}}
|
|
55
|
+
style={[styles.filterButton, btn.isActive && styles.filterButtonActive]}
|
|
56
|
+
activeOpacity={0.7}
|
|
57
|
+
>
|
|
58
|
+
<AtomicIcon
|
|
59
|
+
name={btn.icon}
|
|
60
|
+
size="sm"
|
|
61
|
+
color={btn.isActive ? "primary" : "secondary"}
|
|
62
|
+
/>
|
|
63
|
+
<AtomicText
|
|
64
|
+
style={[styles.filterText, { color: btn.isActive ? tokens.colors.primary : tokens.colors.textSecondary }]}
|
|
65
|
+
>
|
|
66
|
+
{btn.label}
|
|
67
|
+
</AtomicText>
|
|
68
|
+
</TouchableOpacity>
|
|
69
|
+
))}
|
|
66
70
|
</View>
|
|
67
|
-
|
|
71
|
+
)}
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
68
74
|
};
|
|
69
75
|
|
|
70
|
-
const useStyles = (tokens: DesignTokens) =>
|
|
76
|
+
const useStyles = (tokens: DesignTokens) =>
|
|
77
|
+
StyleSheet.create({
|
|
71
78
|
headerArea: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
flexDirection: "row",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
justifyContent: "space-between",
|
|
82
|
+
paddingHorizontal: tokens.spacing.md,
|
|
83
|
+
paddingVertical: tokens.spacing.sm,
|
|
84
|
+
marginBottom: tokens.spacing.sm,
|
|
78
85
|
},
|
|
79
86
|
title: {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
fontSize: 20,
|
|
88
|
+
fontWeight: "700",
|
|
89
|
+
color: tokens.colors.textPrimary,
|
|
90
|
+
marginBottom: 4,
|
|
84
91
|
},
|
|
85
92
|
subtitle: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
fontSize: 14,
|
|
94
|
+
color: tokens.colors.textSecondary,
|
|
95
|
+
opacity: 0.6,
|
|
96
|
+
},
|
|
97
|
+
filterRow: {
|
|
98
|
+
flexDirection: "row",
|
|
99
|
+
gap: tokens.spacing.xs,
|
|
89
100
|
},
|
|
90
101
|
filterButton: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
gap: tokens.spacing.xs,
|
|
105
|
+
paddingVertical: tokens.spacing.xs,
|
|
106
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
107
|
+
borderRadius: 999,
|
|
108
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
109
|
+
borderWidth: 1,
|
|
110
|
+
borderColor: "transparent",
|
|
100
111
|
},
|
|
101
112
|
filterButtonActive: {
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
backgroundColor: tokens.colors.primary + "15",
|
|
114
|
+
borderColor: tokens.colors.primary + "30",
|
|
104
115
|
},
|
|
105
116
|
filterText: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
badge: {
|
|
110
|
-
width: 8,
|
|
111
|
-
height: 8,
|
|
112
|
-
borderRadius: 4,
|
|
113
|
-
backgroundColor: tokens.colors.primary,
|
|
114
|
-
position: 'absolute',
|
|
115
|
-
top: 6,
|
|
116
|
-
right: 6,
|
|
117
|
+
fontSize: 13,
|
|
118
|
+
fontWeight: "500",
|
|
117
119
|
},
|
|
118
|
-
});
|
|
120
|
+
});
|
|
@@ -6,3 +6,6 @@ export { useCreations } from "./useCreations";
|
|
|
6
6
|
export { useDeleteCreation } from "./useDeleteCreation";
|
|
7
7
|
export { useCreationsFilter } from "./useCreationsFilter";
|
|
8
8
|
export { useAdvancedFilter } from "./useAdvancedFilter";
|
|
9
|
+
export { useStatusFilter } from "./useStatusFilter";
|
|
10
|
+
export { useMediaFilter } from "./useMediaFilter";
|
|
11
|
+
export { useGalleryFilters } from "./useGalleryFilters";
|
|
@@ -1,77 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationsFilter Hook
|
|
3
|
-
*
|
|
3
|
+
* Combines status and media filters to filter creations
|
|
4
|
+
* SOLID: Combines filters, delegates individual filter logic to separate hooks
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useMemo } from "react";
|
|
7
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
|
+
import { getCategoryForType } from "../../domain/types/creation-categories";
|
|
8
10
|
|
|
9
11
|
interface UseCreationsFilterProps {
|
|
10
12
|
readonly creations: Creation[] | undefined;
|
|
11
|
-
readonly
|
|
13
|
+
readonly statusFilter?: string;
|
|
14
|
+
readonly mediaFilter?: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
interface
|
|
15
|
-
readonly
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
17
|
+
interface UseCreationsFilterReturn {
|
|
18
|
+
readonly filtered: Creation[];
|
|
19
|
+
readonly isFiltered: boolean;
|
|
20
|
+
readonly activeFiltersCount: number;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function useCreationsFilter({
|
|
22
24
|
creations,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
statusFilter = "all",
|
|
26
|
+
mediaFilter = "all"
|
|
27
|
+
}: UseCreationsFilterProps): UseCreationsFilterReturn {
|
|
26
28
|
|
|
27
29
|
const filtered = useMemo(() => {
|
|
28
30
|
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
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
return creations.filter((creation) => {
|
|
33
|
+
// Status filter
|
|
34
|
+
if (statusFilter !== "all" && creation.status !== statusFilter) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
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];
|
|
38
|
+
// Media filter
|
|
39
|
+
if (mediaFilter !== "all") {
|
|
40
|
+
const category = getCategoryForType(creation.type);
|
|
41
|
+
if (category !== mediaFilter) {
|
|
42
|
+
return false;
|
|
57
43
|
}
|
|
58
44
|
}
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
if (newIds.length === 0) return [defaultFilterId];
|
|
62
|
-
return newIds;
|
|
46
|
+
return true;
|
|
63
47
|
});
|
|
64
|
-
}, [
|
|
48
|
+
}, [creations, statusFilter, mediaFilter]);
|
|
49
|
+
|
|
50
|
+
const activeFiltersCount = useMemo(() => {
|
|
51
|
+
let count = 0;
|
|
52
|
+
if (statusFilter !== "all") count++;
|
|
53
|
+
if (mediaFilter !== "all") count++;
|
|
54
|
+
return count;
|
|
55
|
+
}, [statusFilter, mediaFilter]);
|
|
65
56
|
|
|
66
|
-
const
|
|
67
|
-
setSelectedIds([defaultFilterId]);
|
|
68
|
-
}, [defaultFilterId]);
|
|
57
|
+
const isFiltered = statusFilter !== "all" || mediaFilter !== "all";
|
|
69
58
|
|
|
70
59
|
return {
|
|
71
60
|
filtered,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
clearFilters,
|
|
75
|
-
isFiltered: !selectedIds.includes(defaultFilterId),
|
|
61
|
+
isFiltered,
|
|
62
|
+
activeFiltersCount
|
|
76
63
|
};
|
|
77
64
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGalleryFilters Hook
|
|
3
|
+
* Manages all filter state and modals for gallery screen
|
|
4
|
+
* SOLID: Coordinates filter hooks without business logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
9
|
+
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
10
|
+
import { useStatusFilter } from "./useStatusFilter";
|
|
11
|
+
import { useMediaFilter } from "./useMediaFilter";
|
|
12
|
+
import { useCreationsFilter } from "./useCreationsFilter";
|
|
13
|
+
|
|
14
|
+
interface UseGalleryFiltersProps {
|
|
15
|
+
readonly creations: Creation[] | undefined;
|
|
16
|
+
readonly statusOptions: FilterOption[];
|
|
17
|
+
readonly mediaOptions: FilterOption[];
|
|
18
|
+
readonly t: (key: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UseGalleryFiltersReturn {
|
|
22
|
+
readonly filtered: Creation[];
|
|
23
|
+
readonly isFiltered: boolean;
|
|
24
|
+
readonly activeFiltersCount: number;
|
|
25
|
+
readonly statusFilterVisible: boolean;
|
|
26
|
+
readonly mediaFilterVisible: boolean;
|
|
27
|
+
readonly statusFilter: ReturnType<typeof useStatusFilter>;
|
|
28
|
+
readonly mediaFilter: ReturnType<typeof useMediaFilter>;
|
|
29
|
+
readonly openStatusFilter: () => void;
|
|
30
|
+
readonly closeStatusFilter: () => void;
|
|
31
|
+
readonly openMediaFilter: () => void;
|
|
32
|
+
readonly closeMediaFilter: () => void;
|
|
33
|
+
readonly clearAllFilters: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useGalleryFilters({
|
|
37
|
+
creations,
|
|
38
|
+
statusOptions,
|
|
39
|
+
mediaOptions,
|
|
40
|
+
t
|
|
41
|
+
}: UseGalleryFiltersProps): UseGalleryFiltersReturn {
|
|
42
|
+
const [statusFilterVisible, setStatusFilterVisible] = useState(false);
|
|
43
|
+
const [mediaFilterVisible, setMediaFilterVisible] = useState(false);
|
|
44
|
+
|
|
45
|
+
const statusFilter = useStatusFilter({ options: statusOptions, t });
|
|
46
|
+
const mediaFilter = useMediaFilter({ options: mediaOptions, t });
|
|
47
|
+
|
|
48
|
+
const { filtered, isFiltered, activeFiltersCount } = useCreationsFilter({
|
|
49
|
+
creations,
|
|
50
|
+
statusFilter: statusFilter.selectedId,
|
|
51
|
+
mediaFilter: mediaFilter.selectedId
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const openStatusFilter = useCallback(() => setStatusFilterVisible(true), []);
|
|
55
|
+
const closeStatusFilter = useCallback(() => setStatusFilterVisible(false), []);
|
|
56
|
+
const openMediaFilter = useCallback(() => setMediaFilterVisible(true), []);
|
|
57
|
+
const closeMediaFilter = useCallback(() => setMediaFilterVisible(false), []);
|
|
58
|
+
|
|
59
|
+
const clearAllFilters = useCallback(() => {
|
|
60
|
+
statusFilter.clearFilter();
|
|
61
|
+
mediaFilter.clearFilter();
|
|
62
|
+
}, [statusFilter, mediaFilter]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
filtered,
|
|
66
|
+
isFiltered,
|
|
67
|
+
activeFiltersCount,
|
|
68
|
+
statusFilterVisible,
|
|
69
|
+
mediaFilterVisible,
|
|
70
|
+
statusFilter,
|
|
71
|
+
mediaFilter,
|
|
72
|
+
openStatusFilter,
|
|
73
|
+
closeStatusFilter,
|
|
74
|
+
openMediaFilter,
|
|
75
|
+
closeMediaFilter,
|
|
76
|
+
clearAllFilters
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMediaFilter Hook
|
|
3
|
+
* Handles media type filtering (image, video, voice)
|
|
4
|
+
* SOLID: Single Responsibility - Only handles media filter state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useMemo } from "react";
|
|
8
|
+
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
9
|
+
|
|
10
|
+
interface UseMediaFilterProps {
|
|
11
|
+
readonly options: FilterOption[];
|
|
12
|
+
readonly t: (key: string) => string;
|
|
13
|
+
readonly defaultId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseMediaFilterReturn {
|
|
17
|
+
readonly selectedId: string;
|
|
18
|
+
readonly filterOptions: FilterOption[];
|
|
19
|
+
readonly hasActiveFilter: boolean;
|
|
20
|
+
readonly selectFilter: (id: string) => void;
|
|
21
|
+
readonly clearFilter: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useMediaFilter({
|
|
25
|
+
options,
|
|
26
|
+
t,
|
|
27
|
+
defaultId = "all"
|
|
28
|
+
}: UseMediaFilterProps): UseMediaFilterReturn {
|
|
29
|
+
const [selectedId, setSelectedId] = useState(defaultId);
|
|
30
|
+
|
|
31
|
+
const filterOptions = useMemo(() =>
|
|
32
|
+
options.map(opt => ({
|
|
33
|
+
...opt,
|
|
34
|
+
label: opt.labelKey ? t(opt.labelKey) : opt.label
|
|
35
|
+
})),
|
|
36
|
+
[options, t]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const selectFilter = useCallback((id: string) => {
|
|
40
|
+
setSelectedId(id);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const clearFilter = useCallback(() => {
|
|
44
|
+
setSelectedId(defaultId);
|
|
45
|
+
}, [defaultId]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
selectedId,
|
|
49
|
+
filterOptions,
|
|
50
|
+
hasActiveFilter: selectedId !== defaultId,
|
|
51
|
+
selectFilter,
|
|
52
|
+
clearFilter
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStatusFilter Hook
|
|
3
|
+
* Handles status filtering (completed, pending, processing, failed)
|
|
4
|
+
* SOLID: Single Responsibility - Only handles status filter state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useMemo } from "react";
|
|
8
|
+
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
9
|
+
|
|
10
|
+
interface UseStatusFilterProps {
|
|
11
|
+
readonly options: FilterOption[];
|
|
12
|
+
readonly t: (key: string) => string;
|
|
13
|
+
readonly defaultId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseStatusFilterReturn {
|
|
17
|
+
readonly selectedId: string;
|
|
18
|
+
readonly filterOptions: FilterOption[];
|
|
19
|
+
readonly hasActiveFilter: boolean;
|
|
20
|
+
readonly selectFilter: (id: string) => void;
|
|
21
|
+
readonly clearFilter: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useStatusFilter({
|
|
25
|
+
options,
|
|
26
|
+
t,
|
|
27
|
+
defaultId = "all"
|
|
28
|
+
}: UseStatusFilterProps): UseStatusFilterReturn {
|
|
29
|
+
const [selectedId, setSelectedId] = useState(defaultId);
|
|
30
|
+
|
|
31
|
+
const filterOptions = useMemo(() =>
|
|
32
|
+
options.map(opt => ({
|
|
33
|
+
...opt,
|
|
34
|
+
label: opt.labelKey ? t(opt.labelKey) : opt.label
|
|
35
|
+
})),
|
|
36
|
+
[options, t]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const selectFilter = useCallback((id: string) => {
|
|
40
|
+
setSelectedId(id);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const clearFilter = useCallback(() => {
|
|
44
|
+
setSelectedId(defaultId);
|
|
45
|
+
}, [defaultId]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
selectedId,
|
|
49
|
+
filterOptions,
|
|
50
|
+
hasActiveFilter: selectedId !== defaultId,
|
|
51
|
+
selectFilter,
|
|
52
|
+
clearFilter
|
|
53
|
+
};
|
|
54
|
+
}
|