@umituz/react-native-ai-generation-content 1.17.268 → 1.17.270
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/repositories/ICreationsRepository.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +8 -0
- package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +19 -0
- package/src/domains/creations/presentation/components/CreationRating.tsx +69 -0
- package/src/domains/creations/presentation/components/index.ts +1 -0
- package/src/domains/creations/presentation/hooks/index.ts +1 -0
- package/src/domains/creations/presentation/hooks/useCreationRating.ts +30 -0
- package/src/domains/creations/presentation/hooks/useCreations.ts +2 -2
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +4 -28
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +40 -44
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +22 -8
- package/src/features/scenarios/domain/types.ts +75 -0
- package/src/features/scenarios/index.ts +45 -0
- package/src/features/scenarios/presentation/components/InspirationChips.tsx +82 -0
- package/src/features/scenarios/presentation/components/MagicPromptHeadline.tsx +79 -0
- package/src/features/scenarios/presentation/components/ScenarioGrid.tsx +225 -0
- package/src/features/scenarios/presentation/components/ScenarioHeader.tsx +56 -0
- package/src/features/scenarios/presentation/components/StyleSelector.tsx +119 -0
- package/src/features/scenarios/presentation/components/index.ts +18 -0
- package/src/features/scenarios/presentation/screens/MagicPromptScreen.tsx +242 -0
- package/src/features/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +164 -0
- package/src/features/scenarios/presentation/screens/ScenarioSelectorScreen.tsx +65 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.270",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -97,4 +97,12 @@ export class CreationsRepository
|
|
|
97
97
|
): Promise<boolean> {
|
|
98
98
|
return this.writer.updateFavorite(userId, creationId, isFavorite);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
async rate(
|
|
102
|
+
userId: string,
|
|
103
|
+
creationId: string,
|
|
104
|
+
rating: number,
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
return this.writer.rate(userId, creationId, rating);
|
|
107
|
+
}
|
|
100
108
|
}
|
|
@@ -148,4 +148,23 @@ export class CreationsWriter {
|
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
async rate(
|
|
153
|
+
userId: string,
|
|
154
|
+
creationId: string,
|
|
155
|
+
rating: number,
|
|
156
|
+
): Promise<boolean> {
|
|
157
|
+
const docRef = this.pathResolver.getDocRef(userId, creationId);
|
|
158
|
+
if (!docRef) return false;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await updateDoc(docRef, {
|
|
162
|
+
rating,
|
|
163
|
+
ratedAt: new Date(),
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
151
170
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
|
|
5
|
+
export interface CreationRatingProps {
|
|
6
|
+
readonly rating: number;
|
|
7
|
+
readonly max?: number;
|
|
8
|
+
readonly size?: number;
|
|
9
|
+
readonly onRate?: (rating: number) => void;
|
|
10
|
+
readonly readonly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CreationRating: React.FC<CreationRatingProps> = ({
|
|
14
|
+
rating,
|
|
15
|
+
max = 5,
|
|
16
|
+
size = 32,
|
|
17
|
+
onRate,
|
|
18
|
+
readonly = false,
|
|
19
|
+
}) => {
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.container}>
|
|
24
|
+
<View style={styles.stars}>
|
|
25
|
+
{Array.from({ length: max }).map((_, i) => {
|
|
26
|
+
const isFilled = i < rating;
|
|
27
|
+
return (
|
|
28
|
+
<TouchableOpacity
|
|
29
|
+
key={i}
|
|
30
|
+
onPress={() => onRate?.(i + 1)}
|
|
31
|
+
activeOpacity={0.7}
|
|
32
|
+
disabled={readonly}
|
|
33
|
+
style={styles.star}
|
|
34
|
+
>
|
|
35
|
+
<AtomicIcon
|
|
36
|
+
name={isFilled ? "star" : "star-outline"}
|
|
37
|
+
customSize={size}
|
|
38
|
+
customColor={isFilled ? tokens.colors.primary : tokens.colors.textTertiary}
|
|
39
|
+
/>
|
|
40
|
+
</TouchableOpacity>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</View>
|
|
44
|
+
{!readonly && rating > 0 && (
|
|
45
|
+
<AtomicText variant="bodySmall" color="textSecondary" style={styles.valueText}>
|
|
46
|
+
{rating} / {max}
|
|
47
|
+
</AtomicText>
|
|
48
|
+
)}
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
container: {
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
paddingVertical: 12,
|
|
57
|
+
},
|
|
58
|
+
stars: {
|
|
59
|
+
flexDirection: "row",
|
|
60
|
+
gap: 8,
|
|
61
|
+
},
|
|
62
|
+
star: {
|
|
63
|
+
padding: 2,
|
|
64
|
+
},
|
|
65
|
+
valueText: {
|
|
66
|
+
marginTop: 8,
|
|
67
|
+
fontWeight: "600",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -30,6 +30,7 @@ export { GalleryHeader } from "./GalleryHeader";
|
|
|
30
30
|
export { EmptyState } from "./EmptyState";
|
|
31
31
|
export { GalleryEmptyStates } from "./GalleryEmptyStates";
|
|
32
32
|
export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
33
|
+
export { CreationRating } from "./CreationRating";
|
|
33
34
|
export { CreationsGrid } from "./CreationsGrid";
|
|
34
35
|
|
|
35
36
|
// Detail Components
|
|
@@ -15,6 +15,7 @@ export { useFilter, useStatusFilter, useMediaFilter } from "./useFilter";
|
|
|
15
15
|
export type { UseFilterProps, UseFilterReturn } from "./useFilter";
|
|
16
16
|
export { useGalleryFilters } from "./useGalleryFilters";
|
|
17
17
|
export { useCreationPersistence } from "./useCreationPersistence";
|
|
18
|
+
export { useCreationRating } from "./useCreationRating";
|
|
18
19
|
export type {
|
|
19
20
|
UseCreationPersistenceConfig,
|
|
20
21
|
UseCreationPersistenceReturn,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreationRating Hook
|
|
3
|
+
* Handles rating of creations with optimistic update
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useOptimisticUpdate } from "@umituz/react-native-design-system";
|
|
7
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
9
|
+
|
|
10
|
+
interface UseCreationRatingProps {
|
|
11
|
+
readonly userId: string | null;
|
|
12
|
+
readonly repository: ICreationsRepository;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCreationRating({
|
|
16
|
+
userId,
|
|
17
|
+
repository,
|
|
18
|
+
}: UseCreationRatingProps) {
|
|
19
|
+
return useOptimisticUpdate<boolean, { id: string; rating: number }, Creation[]>({
|
|
20
|
+
mutationFn: async ({ id, rating }) => {
|
|
21
|
+
if (!userId) return false;
|
|
22
|
+
return repository.rate(userId, id, rating);
|
|
23
|
+
},
|
|
24
|
+
queryKey: ["creations", userId ?? ""],
|
|
25
|
+
updateFn: (old, { id, rating }) =>
|
|
26
|
+
old?.map((c) =>
|
|
27
|
+
c.id === id ? { ...c, rating, ratedAt: new Date() } : c
|
|
28
|
+
) ?? [],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Fetches user's creations from repository
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useAppQuery } from "@umituz/react-native-design-system";
|
|
7
7
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
|
|
@@ -23,7 +23,7 @@ export function useCreations({
|
|
|
23
23
|
repository,
|
|
24
24
|
enabled = true,
|
|
25
25
|
}: UseCreationsProps) {
|
|
26
|
-
return
|
|
26
|
+
return useAppQuery<Creation[]>({
|
|
27
27
|
queryKey: ["creations", userId ?? ""],
|
|
28
28
|
queryFn: async () => {
|
|
29
29
|
if (!userId) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles deletion of user creations with optimistic update
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useOptimisticUpdate } from "@umituz/react-native-design-system";
|
|
7
7
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
|
|
@@ -16,36 +16,12 @@ export function useDeleteCreation({
|
|
|
16
16
|
userId,
|
|
17
17
|
repository,
|
|
18
18
|
}: UseDeleteCreationProps) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return useMutation({
|
|
19
|
+
return useOptimisticUpdate<boolean, string, Creation[]>({
|
|
22
20
|
mutationFn: async (creationId: string) => {
|
|
23
21
|
if (!userId) return false;
|
|
24
22
|
return repository.delete(userId, creationId);
|
|
25
23
|
},
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
},
|
|
24
|
+
queryKey: ["creations", userId ?? ""],
|
|
25
|
+
updateFn: (old, creationId) => old?.filter((c) => c.id !== creationId) ?? [],
|
|
50
26
|
});
|
|
51
27
|
}
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
2
|
+
import { } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens, ScreenLayout } from "@umituz/react-native-design-system";
|
|
5
4
|
import type { Creation } from '../../domain/entities/Creation';
|
|
6
5
|
import type { CreationsConfig } from '../../domain/value-objects/CreationsConfig';
|
|
6
|
+
import type { ICreationsRepository } from '../../domain/repositories/ICreationsRepository';
|
|
7
7
|
import { hasVideoContent, getPreviewUrl } from '../../domain/utils';
|
|
8
|
+
import { useCreationRating } from '../hooks/useCreationRating';
|
|
8
9
|
import { DetailHeader } from '../components/CreationDetail/DetailHeader';
|
|
9
10
|
import { DetailInfo } from '../components/CreationDetail/DetailInfo';
|
|
10
11
|
import { DetailImage } from '../components/CreationDetail/DetailImage';
|
|
11
12
|
import { DetailVideo } from '../components/CreationDetail/DetailVideo';
|
|
12
13
|
import { DetailStory } from '../components/CreationDetail/DetailStory';
|
|
13
14
|
import { DetailActions } from '../components/CreationDetail/DetailActions';
|
|
15
|
+
import { CreationRating } from '../components/CreationRating';
|
|
14
16
|
import { getLocalizedTitle } from '../utils/filterUtils';
|
|
15
17
|
|
|
16
18
|
/** Video creation types */
|
|
17
19
|
const VIDEO_TYPES = ['text-to-video', 'image-to-video'] as const;
|
|
18
20
|
|
|
19
21
|
interface CreationDetailScreenProps {
|
|
22
|
+
readonly userId: string | null;
|
|
23
|
+
readonly repository: ICreationsRepository;
|
|
20
24
|
readonly creation: Creation;
|
|
21
25
|
readonly config: CreationsConfig;
|
|
22
26
|
readonly onClose: () => void;
|
|
@@ -34,6 +38,8 @@ interface CreationMetadata {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
41
|
+
userId,
|
|
42
|
+
repository,
|
|
37
43
|
creation,
|
|
38
44
|
config,
|
|
39
45
|
onClose,
|
|
@@ -43,7 +49,11 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
|
43
49
|
t
|
|
44
50
|
}) => {
|
|
45
51
|
const tokens = useAppDesignTokens();
|
|
46
|
-
const
|
|
52
|
+
const rateMutation = useCreationRating({ userId, repository });
|
|
53
|
+
|
|
54
|
+
const handleRate = async (rating: number) => {
|
|
55
|
+
await rateMutation.mutateAsync({ id: creation.id, rating });
|
|
56
|
+
};
|
|
47
57
|
|
|
48
58
|
// Extract data safely
|
|
49
59
|
const metadata = (creation.metadata || {}) as CreationMetadata;
|
|
@@ -68,48 +78,34 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
|
68
78
|
const thumbnailUrl = getPreviewUrl(creation.output) || undefined;
|
|
69
79
|
|
|
70
80
|
return (
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<DetailVideo videoUrl={videoUrl} _thumbnailUrl={thumbnailUrl} />
|
|
82
|
-
) : (
|
|
83
|
-
<DetailImage uri={creation.uri} />
|
|
84
|
-
)}
|
|
81
|
+
<ScreenLayout
|
|
82
|
+
header={<DetailHeader onClose={onClose} />}
|
|
83
|
+
>
|
|
84
|
+
{isVideo ? (
|
|
85
|
+
<DetailVideo videoUrl={videoUrl} _thumbnailUrl={thumbnailUrl} />
|
|
86
|
+
) : (
|
|
87
|
+
<DetailImage uri={creation.uri} />
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<DetailInfo title={title} date={date} />
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
<CreationRating
|
|
93
|
+
rating={creation.rating || 0}
|
|
94
|
+
onRate={handleRate}
|
|
95
|
+
/>
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
{story ? (
|
|
98
|
+
<DetailStory story={story} />
|
|
99
|
+
) : null}
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</View>
|
|
101
|
+
<DetailActions
|
|
102
|
+
onShare={() => onShare(creation)}
|
|
103
|
+
onDelete={() => onDelete(creation)}
|
|
104
|
+
onViewResult={onViewResult ? () => onViewResult(creation) : undefined}
|
|
105
|
+
shareLabel={t("result.shareButton")}
|
|
106
|
+
deleteLabel={t("common.delete")}
|
|
107
|
+
viewResultLabel={onViewResult ? t("result.viewResult") : undefined}
|
|
108
|
+
/>
|
|
109
|
+
</ScreenLayout>
|
|
102
110
|
);
|
|
103
111
|
};
|
|
104
|
-
|
|
105
|
-
const styles = StyleSheet.create({
|
|
106
|
-
container: {
|
|
107
|
-
flex: 1,
|
|
108
|
-
},
|
|
109
|
-
scrollView: {
|
|
110
|
-
flex: 1,
|
|
111
|
-
},
|
|
112
|
-
scrollContent: {
|
|
113
|
-
paddingTop: 4,
|
|
114
|
-
},
|
|
115
|
-
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback } from "react";
|
|
2
2
|
import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
|
|
3
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
3
|
import {
|
|
5
4
|
useAppDesignTokens,
|
|
6
5
|
useAlert,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
AlertMode,
|
|
9
8
|
useSharing,
|
|
10
9
|
FilterSheet,
|
|
10
|
+
ScreenLayout,
|
|
11
11
|
} from "@umituz/react-native-design-system";
|
|
12
12
|
import { useFocusEffect } from "@react-navigation/native";
|
|
13
13
|
import { useCreations } from "../hooks/useCreations";
|
|
@@ -42,7 +42,6 @@ export function CreationsGalleryScreen({
|
|
|
42
42
|
showFilter = config.showFilter ?? true,
|
|
43
43
|
onViewResult,
|
|
44
44
|
}: CreationsGalleryScreenProps) {
|
|
45
|
-
const insets = useSafeAreaInsets();
|
|
46
45
|
const tokens = useAppDesignTokens();
|
|
47
46
|
const { share } = useSharing();
|
|
48
47
|
const alert = useAlert();
|
|
@@ -123,7 +122,7 @@ export function CreationsGalleryScreen({
|
|
|
123
122
|
const renderHeader = useMemo(() => {
|
|
124
123
|
if ((!creations || creations.length === 0) && !isLoading) return null;
|
|
125
124
|
return (
|
|
126
|
-
<View style={[styles.header, {
|
|
125
|
+
<View style={[styles.header, { backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
|
|
127
126
|
<GalleryHeader
|
|
128
127
|
title={t(config.translations.title)}
|
|
129
128
|
count={filters.filtered.length}
|
|
@@ -133,7 +132,7 @@ export function CreationsGalleryScreen({
|
|
|
133
132
|
/>
|
|
134
133
|
</View>
|
|
135
134
|
);
|
|
136
|
-
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config,
|
|
135
|
+
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens]);
|
|
137
136
|
|
|
138
137
|
const renderEmpty = useMemo(() => (
|
|
139
138
|
<GalleryEmptyStates
|
|
@@ -151,24 +150,39 @@ export function CreationsGalleryScreen({
|
|
|
151
150
|
), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
|
|
152
151
|
|
|
153
152
|
if (selectedCreation) {
|
|
154
|
-
return
|
|
153
|
+
return (
|
|
154
|
+
<CreationDetailScreen
|
|
155
|
+
userId={userId}
|
|
156
|
+
repository={repository}
|
|
157
|
+
creation={selectedCreation}
|
|
158
|
+
config={config}
|
|
159
|
+
onClose={() => setSelectedCreation(null)}
|
|
160
|
+
onShare={handleShare}
|
|
161
|
+
onDelete={handleDelete}
|
|
162
|
+
onViewResult={onViewResult}
|
|
163
|
+
t={t}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
return (
|
|
158
|
-
<
|
|
169
|
+
<ScreenLayout>
|
|
159
170
|
<FlatList
|
|
160
171
|
data={filters.filtered}
|
|
161
172
|
renderItem={renderItem}
|
|
162
173
|
keyExtractor={(item) => item.id}
|
|
163
174
|
ListHeaderComponent={renderHeader}
|
|
164
175
|
ListEmptyComponent={renderEmpty}
|
|
165
|
-
contentContainerStyle={[
|
|
176
|
+
contentContainerStyle={[
|
|
177
|
+
styles.listContent,
|
|
178
|
+
(!filters.filtered || filters.filtered.length === 0) && styles.emptyContent
|
|
179
|
+
]}
|
|
166
180
|
showsVerticalScrollIndicator={false}
|
|
167
181
|
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={() => void refetch()} tintColor={tokens.colors.primary} />}
|
|
168
182
|
/>
|
|
169
183
|
<FilterSheet visible={filters.statusFilterVisible} onClose={filters.closeStatusFilter} options={filters.statusFilter.filterOptions} selectedIds={[filters.statusFilter.selectedId]} onFilterPress={filters.statusFilter.selectFilter} onClearFilters={filters.statusFilter.clearFilter} title={t(config.translations.statusFilterTitle ?? "creations.filter.status")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
|
|
170
184
|
<FilterSheet visible={filters.mediaFilterVisible} onClose={filters.closeMediaFilter} options={filters.mediaFilter.filterOptions} selectedIds={[filters.mediaFilter.selectedId]} onFilterPress={filters.mediaFilter.selectFilter} onClearFilters={filters.mediaFilter.clearFilter} title={t(config.translations.mediaFilterTitle ?? "creations.filter.media")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
|
|
171
|
-
</
|
|
185
|
+
</ScreenLayout>
|
|
172
186
|
);
|
|
173
187
|
}
|
|
174
188
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario Domain Types
|
|
3
|
+
* Generic types for scenario selection feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum ScenarioCategory {
|
|
7
|
+
TIME_TRAVEL = "time_travel",
|
|
8
|
+
FAMILY = "family",
|
|
9
|
+
LIFESTYLE = "lifestyle",
|
|
10
|
+
FANTASY = "fantasy",
|
|
11
|
+
CAREER = "career",
|
|
12
|
+
TRAVEL = "travel",
|
|
13
|
+
CULTURAL = "cultural",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ScenarioData {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly category?: ScenarioCategory;
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly icon: string;
|
|
22
|
+
readonly imageUrl?: string;
|
|
23
|
+
readonly previewImageUrl?: string;
|
|
24
|
+
readonly aiPrompt: string;
|
|
25
|
+
readonly storyTemplate: string;
|
|
26
|
+
readonly requiresPhoto?: boolean;
|
|
27
|
+
readonly hidden?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScenarioSelectorConfig {
|
|
31
|
+
readonly titleKey: string;
|
|
32
|
+
readonly subtitleKey: string;
|
|
33
|
+
readonly showCategoryFilter?: boolean;
|
|
34
|
+
readonly enableSearch?: boolean;
|
|
35
|
+
readonly pageSize?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScenarioPreviewConfig {
|
|
39
|
+
readonly showTips?: boolean;
|
|
40
|
+
readonly showDetails?: boolean;
|
|
41
|
+
readonly enableCustomization?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MagicPromptConfig {
|
|
45
|
+
readonly maxLength: number;
|
|
46
|
+
readonly minLength: number;
|
|
47
|
+
readonly headerKey: string;
|
|
48
|
+
readonly headlinePart1Key: string;
|
|
49
|
+
readonly headlinePart2Key: string;
|
|
50
|
+
readonly subtitleKey: string;
|
|
51
|
+
readonly inputLabelKey: string;
|
|
52
|
+
readonly surpriseButtonKey: string;
|
|
53
|
+
readonly placeholderKey: string;
|
|
54
|
+
readonly styleTitleKey: string;
|
|
55
|
+
readonly inspirationTitleKey: string;
|
|
56
|
+
readonly continueKey: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VisualStyleOption {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly icon: string;
|
|
62
|
+
readonly labelKey: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InspirationChipData {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly labelKey: string;
|
|
68
|
+
readonly promptKey: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const SCENARIO_DEFAULTS = {
|
|
72
|
+
pageSize: 10,
|
|
73
|
+
maxPromptLength: 500,
|
|
74
|
+
minPromptLength: 10,
|
|
75
|
+
} as const;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenarios Feature
|
|
3
|
+
* Config-driven scenario selection and preview screens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Domain types
|
|
7
|
+
export type {
|
|
8
|
+
ScenarioData,
|
|
9
|
+
ScenarioCategory,
|
|
10
|
+
ScenarioSelectorConfig,
|
|
11
|
+
ScenarioPreviewConfig,
|
|
12
|
+
MagicPromptConfig,
|
|
13
|
+
VisualStyleOption,
|
|
14
|
+
InspirationChipData,
|
|
15
|
+
} from "./domain/types";
|
|
16
|
+
export { ScenarioCategory as ScenarioCategoryEnum, SCENARIO_DEFAULTS } from "./domain/types";
|
|
17
|
+
|
|
18
|
+
// Components
|
|
19
|
+
export {
|
|
20
|
+
ScenarioHeader,
|
|
21
|
+
ScenarioGrid,
|
|
22
|
+
MagicPromptHeadline,
|
|
23
|
+
InspirationChips,
|
|
24
|
+
StyleSelector,
|
|
25
|
+
} from "./presentation/components";
|
|
26
|
+
export type {
|
|
27
|
+
ScenarioHeaderProps,
|
|
28
|
+
ScenarioGridProps,
|
|
29
|
+
MagicPromptHeadlineProps,
|
|
30
|
+
InspirationChipsProps,
|
|
31
|
+
StyleSelectorProps,
|
|
32
|
+
} from "./presentation/components";
|
|
33
|
+
|
|
34
|
+
// Screens
|
|
35
|
+
export { ScenarioSelectorScreen } from "./presentation/screens/ScenarioSelectorScreen";
|
|
36
|
+
export type { ScenarioSelectorScreenProps } from "./presentation/screens/ScenarioSelectorScreen";
|
|
37
|
+
|
|
38
|
+
export { ScenarioPreviewScreen } from "./presentation/screens/ScenarioPreviewScreen";
|
|
39
|
+
export type {
|
|
40
|
+
ScenarioPreviewScreenProps,
|
|
41
|
+
ScenarioPreviewTranslations,
|
|
42
|
+
} from "./presentation/screens/ScenarioPreviewScreen";
|
|
43
|
+
|
|
44
|
+
export { MagicPromptScreen } from "./presentation/screens/MagicPromptScreen";
|
|
45
|
+
export type { MagicPromptScreenProps } from "./presentation/screens/MagicPromptScreen";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InspirationChips Component
|
|
3
|
+
* Horizontal scrollable suggestion chips for Magic Prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { InspirationChipData } from "../../domain/types";
|
|
13
|
+
|
|
14
|
+
export interface InspirationChipsProps {
|
|
15
|
+
readonly chips: readonly InspirationChipData[];
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly onSelect: (promptKey: string) => void;
|
|
18
|
+
readonly t: (key: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const InspirationChips: React.FC<InspirationChipsProps> = ({
|
|
22
|
+
chips,
|
|
23
|
+
title,
|
|
24
|
+
onSelect,
|
|
25
|
+
t,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
|
|
29
|
+
const styles = useMemo(
|
|
30
|
+
() =>
|
|
31
|
+
StyleSheet.create({
|
|
32
|
+
container: {
|
|
33
|
+
marginBottom: tokens.spacing.lg,
|
|
34
|
+
},
|
|
35
|
+
sectionTitle: {
|
|
36
|
+
fontWeight: "700",
|
|
37
|
+
marginBottom: tokens.spacing.sm,
|
|
38
|
+
},
|
|
39
|
+
chipsContainer: {
|
|
40
|
+
gap: tokens.spacing.sm,
|
|
41
|
+
paddingBottom: 4,
|
|
42
|
+
},
|
|
43
|
+
chip: {
|
|
44
|
+
paddingHorizontal: tokens.spacing.md,
|
|
45
|
+
paddingVertical: 10,
|
|
46
|
+
borderRadius: 999,
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: tokens.colors.border,
|
|
49
|
+
backgroundColor: tokens.colors.surface,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
[tokens],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View style={styles.container}>
|
|
57
|
+
<AtomicText type="labelLarge" style={styles.sectionTitle}>
|
|
58
|
+
{title}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
<ScrollView
|
|
61
|
+
horizontal
|
|
62
|
+
showsHorizontalScrollIndicator={false}
|
|
63
|
+
contentContainerStyle={styles.chipsContainer}
|
|
64
|
+
>
|
|
65
|
+
{chips.map((chip) => (
|
|
66
|
+
<TouchableOpacity
|
|
67
|
+
key={chip.id}
|
|
68
|
+
style={styles.chip}
|
|
69
|
+
onPress={() => onSelect(chip.promptKey)}
|
|
70
|
+
>
|
|
71
|
+
<AtomicText
|
|
72
|
+
type="bodySmall"
|
|
73
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
74
|
+
>
|
|
75
|
+
{t(chip.labelKey)}
|
|
76
|
+
</AtomicText>
|
|
77
|
+
</TouchableOpacity>
|
|
78
|
+
))}
|
|
79
|
+
</ScrollView>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|