@umituz/react-native-ai-generation-content 1.12.25 → 1.12.29
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 +3 -2
- package/src/domains/creations/application/services/CreationsService.ts +73 -0
- package/src/domains/creations/domain/entities/Creation.ts +60 -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 +75 -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 +241 -0
- package/src/domains/creations/infrastructure/repositories/index.ts +8 -0
- package/src/domains/creations/infrastructure/services/CreationsStorageService.ts +49 -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 +82 -0
- package/src/domains/creations/presentation/components/FilterBottomSheet.tsx +160 -0
- package/src/domains/creations/presentation/components/FilterChips.tsx +105 -0
- package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +87 -0
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +106 -0
- package/src/domains/creations/presentation/components/index.ts +20 -0
- package/src/domains/creations/presentation/hooks/index.ts +7 -0
- package/src/domains/creations/presentation/hooks/useCreations.ts +38 -0
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +77 -0
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +51 -0
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +78 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +194 -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 +42 -0
- package/src/domains/prompts/infrastructure/services/AIServiceProcessor.ts +142 -0
- package/src/domains/prompts/presentation/hooks/useAIServices.ts +15 -132
- package/src/features/background/presentation/components/ComparisonSlider.tsx +2 -2
- package/src/features/background/presentation/components/ErrorDisplay.tsx +2 -2
- package/src/features/background/presentation/components/GenerateButton.tsx +0 -2
- package/src/features/background/presentation/components/ImagePicker.tsx +2 -2
- package/src/features/background/presentation/components/ResultDisplay.tsx +2 -2
- package/src/index.ts +6 -0
- package/src/infrastructure/services/generation-orchestrator.service.ts +25 -162
- package/src/infrastructure/services/job-poller.ts +103 -0
- package/src/infrastructure/services/progress-manager.ts +58 -0
- package/src/infrastructure/services/provider-validator.ts +52 -0
- package/src/presentation/components/GenerationProgressContent.tsx +4 -4
- package/src/presentation/components/PendingJobCard.tsx +2 -2
- package/src/presentation/components/PendingJobCardActions.tsx +2 -2
- package/src/presentation/components/result/GenerationResultContent.tsx +2 -3
- package/src/presentation/hooks/usePhotoGeneration.ts +7 -5
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadFile,
|
|
3
|
+
uploadBase64Image,
|
|
4
|
+
} from "@umituz/react-native-firebase";
|
|
5
|
+
import type { ICreationsStorageService } from "../../domain/services/ICreationsStorageService";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
8
|
+
|
|
9
|
+
export class CreationsStorageService implements ICreationsStorageService {
|
|
10
|
+
constructor(private readonly storagePathPrefix: string = "creations") { }
|
|
11
|
+
|
|
12
|
+
private getPath(userId: string, creationId: string): string {
|
|
13
|
+
return `${this.storagePathPrefix}/${userId}/${creationId}.jpg`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async uploadCreationImage(
|
|
17
|
+
userId: string,
|
|
18
|
+
creationId: string,
|
|
19
|
+
imageUri: string,
|
|
20
|
+
mimeType = "image/jpeg"
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const path = this.getPath(userId, creationId);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (imageUri.startsWith("data:")) {
|
|
26
|
+
const result = await uploadBase64Image(imageUri, path, { mimeType });
|
|
27
|
+
return result.downloadUrl;
|
|
28
|
+
}
|
|
29
|
+
const result = await uploadFile(imageUri, path, { mimeType });
|
|
30
|
+
return result.downloadUrl;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (__DEV__) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error("[CreationsStorageService] upload failed", error);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async deleteCreationImage(
|
|
41
|
+
_userId: string,
|
|
42
|
+
_creationId: string
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
// Delete logic not strictly required for saving loop, but good to have
|
|
45
|
+
// Needs storage reference delete implementation in rn-firebase first
|
|
46
|
+
// For now we skip implementing delete in this iteration as priority is saving
|
|
47
|
+
return Promise.resolve(true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationCard Component
|
|
3
|
+
* Displays a creation item with actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import { View, Image, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
14
|
+
import type { CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
15
|
+
|
|
16
|
+
interface CreationCardProps {
|
|
17
|
+
readonly creation: Creation;
|
|
18
|
+
readonly types: readonly CreationType[];
|
|
19
|
+
readonly onView?: (creation: Creation) => void;
|
|
20
|
+
readonly onShare: (creation: Creation) => void;
|
|
21
|
+
readonly onDelete: (creation: Creation) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CreationCard({
|
|
25
|
+
creation,
|
|
26
|
+
types,
|
|
27
|
+
onView,
|
|
28
|
+
onShare,
|
|
29
|
+
onDelete,
|
|
30
|
+
}: CreationCardProps) {
|
|
31
|
+
const tokens = useAppDesignTokens();
|
|
32
|
+
|
|
33
|
+
const typeConfig = types.find((t) => t.id === creation.type);
|
|
34
|
+
const icon = typeConfig?.icon || "🎨";
|
|
35
|
+
const label = typeConfig?.labelKey || creation.type;
|
|
36
|
+
|
|
37
|
+
const handleView = useCallback(() => onView?.(creation), [creation, onView]);
|
|
38
|
+
const handleShare = useCallback(() => onShare(creation), [creation, onShare]);
|
|
39
|
+
const handleDelete = useCallback(
|
|
40
|
+
() => onDelete(creation),
|
|
41
|
+
[creation, onDelete],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const formattedDate = useMemo(() => {
|
|
45
|
+
const date =
|
|
46
|
+
creation.createdAt instanceof Date
|
|
47
|
+
? creation.createdAt
|
|
48
|
+
: new Date(creation.createdAt);
|
|
49
|
+
return date.toLocaleDateString(undefined, {
|
|
50
|
+
day: "numeric",
|
|
51
|
+
month: "short",
|
|
52
|
+
hour: "2-digit",
|
|
53
|
+
minute: "2-digit",
|
|
54
|
+
});
|
|
55
|
+
}, [creation.createdAt]);
|
|
56
|
+
|
|
57
|
+
const styles = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
backgroundColor: tokens.colors.surface,
|
|
63
|
+
borderRadius: tokens.spacing.md,
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
marginBottom: tokens.spacing.md,
|
|
66
|
+
},
|
|
67
|
+
thumbnail: {
|
|
68
|
+
width: 100,
|
|
69
|
+
height: 100,
|
|
70
|
+
},
|
|
71
|
+
content: {
|
|
72
|
+
flex: 1,
|
|
73
|
+
padding: tokens.spacing.md,
|
|
74
|
+
justifyContent: "space-between",
|
|
75
|
+
},
|
|
76
|
+
typeRow: {
|
|
77
|
+
flexDirection: "row",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
gap: tokens.spacing.sm,
|
|
80
|
+
},
|
|
81
|
+
icon: {
|
|
82
|
+
fontSize: 20,
|
|
83
|
+
},
|
|
84
|
+
typeText: {
|
|
85
|
+
...tokens.typography.bodyMedium,
|
|
86
|
+
fontWeight: "600",
|
|
87
|
+
color: tokens.colors.textPrimary,
|
|
88
|
+
},
|
|
89
|
+
dateText: {
|
|
90
|
+
...tokens.typography.bodySmall,
|
|
91
|
+
color: tokens.colors.textSecondary,
|
|
92
|
+
},
|
|
93
|
+
actions: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
gap: tokens.spacing.sm,
|
|
96
|
+
},
|
|
97
|
+
actionBtn: {
|
|
98
|
+
width: 36,
|
|
99
|
+
height: 36,
|
|
100
|
+
borderRadius: 18,
|
|
101
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
102
|
+
justifyContent: "center",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
[tokens],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View style={styles.container}>
|
|
111
|
+
<Image source={{ uri: creation.uri }} style={styles.thumbnail} />
|
|
112
|
+
<View style={styles.content}>
|
|
113
|
+
<View>
|
|
114
|
+
<View style={styles.typeRow}>
|
|
115
|
+
<AtomicText style={styles.icon}>{icon}</AtomicText>
|
|
116
|
+
<AtomicText style={styles.typeText}>{label}</AtomicText>
|
|
117
|
+
</View>
|
|
118
|
+
<AtomicText style={styles.dateText}>{formattedDate}</AtomicText>
|
|
119
|
+
</View>
|
|
120
|
+
<View style={styles.actions}>
|
|
121
|
+
{onView && (
|
|
122
|
+
<TouchableOpacity style={styles.actionBtn} onPress={handleView}>
|
|
123
|
+
<AtomicIcon name="eye" size="sm" color="primary" />
|
|
124
|
+
</TouchableOpacity>
|
|
125
|
+
)}
|
|
126
|
+
<TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
|
|
127
|
+
<AtomicIcon name="share-social" size="sm" color="primary" />
|
|
128
|
+
</TouchableOpacity>
|
|
129
|
+
<TouchableOpacity style={styles.actionBtn} onPress={handleDelete}>
|
|
130
|
+
<AtomicIcon name="trash" size="sm" color="error" />
|
|
131
|
+
</TouchableOpacity>
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
4
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
|
|
6
|
+
interface DetailActionsProps {
|
|
7
|
+
readonly onShare: () => void;
|
|
8
|
+
readonly onDelete: () => void;
|
|
9
|
+
readonly shareLabel: string;
|
|
10
|
+
readonly deleteLabel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DetailActions: React.FC<DetailActionsProps> = ({
|
|
14
|
+
onShare,
|
|
15
|
+
onDelete,
|
|
16
|
+
shareLabel,
|
|
17
|
+
deleteLabel
|
|
18
|
+
}) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
const styles = useStyles(tokens);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.container}>
|
|
24
|
+
<TouchableOpacity
|
|
25
|
+
style={[styles.button, styles.shareButton]}
|
|
26
|
+
onPress={onShare}
|
|
27
|
+
activeOpacity={0.7}
|
|
28
|
+
>
|
|
29
|
+
<AtomicIcon name="share-social-outline" size="sm" color="onPrimary" />
|
|
30
|
+
<AtomicText style={styles.buttonText}>{shareLabel}</AtomicText>
|
|
31
|
+
</TouchableOpacity>
|
|
32
|
+
|
|
33
|
+
<TouchableOpacity
|
|
34
|
+
style={[styles.button, styles.deleteButton]}
|
|
35
|
+
onPress={onDelete}
|
|
36
|
+
activeOpacity={0.7}
|
|
37
|
+
>
|
|
38
|
+
<AtomicIcon name="trash-outline" size="sm" color="error" />
|
|
39
|
+
<AtomicText style={[styles.buttonText, { color: tokens.colors.error }]}>
|
|
40
|
+
{deleteLabel}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
gap: tokens.spacing.md,
|
|
52
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
53
|
+
marginBottom: tokens.spacing.xxl,
|
|
54
|
+
},
|
|
55
|
+
button: {
|
|
56
|
+
flexDirection: 'row',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
paddingVertical: 12,
|
|
60
|
+
paddingHorizontal: 24,
|
|
61
|
+
borderRadius: 16,
|
|
62
|
+
gap: 8,
|
|
63
|
+
minWidth: 120,
|
|
64
|
+
},
|
|
65
|
+
shareButton: {
|
|
66
|
+
backgroundColor: tokens.colors.primary,
|
|
67
|
+
},
|
|
68
|
+
deleteButton: {
|
|
69
|
+
backgroundColor: tokens.colors.error + '10',
|
|
70
|
+
},
|
|
71
|
+
buttonText: {
|
|
72
|
+
fontWeight: '700',
|
|
73
|
+
color: tokens.colors.textInverse,
|
|
74
|
+
fontSize: 15,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
4
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
import { type EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
|
+
|
|
7
|
+
interface DetailHeaderProps {
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly date: string;
|
|
10
|
+
readonly onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DetailHeader: React.FC<DetailHeaderProps> = ({ title, date, onClose }) => {
|
|
14
|
+
const tokens = useAppDesignTokens();
|
|
15
|
+
const insets = useSafeAreaInsets();
|
|
16
|
+
const styles = useStyles(tokens, insets);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.headerContainer}>
|
|
20
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
21
|
+
<AtomicIcon name="arrow-back" size="md" color="onSurface" />
|
|
22
|
+
</TouchableOpacity>
|
|
23
|
+
|
|
24
|
+
<View style={styles.titleContainer}>
|
|
25
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
26
|
+
<View style={styles.dateBadge}>
|
|
27
|
+
<AtomicIcon name="calendar-outline" size="md" color="primary" />
|
|
28
|
+
<AtomicText style={styles.dateText}>{date}</AtomicText>
|
|
29
|
+
</View>
|
|
30
|
+
</View>
|
|
31
|
+
|
|
32
|
+
<View style={styles.placeholder} />
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const useStyles = (tokens: DesignTokens, insets: EdgeInsets) => StyleSheet.create({
|
|
38
|
+
headerContainer: {
|
|
39
|
+
flexDirection: 'row',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
paddingTop: insets.top + tokens.spacing.sm,
|
|
42
|
+
paddingBottom: tokens.spacing.md,
|
|
43
|
+
paddingHorizontal: tokens.spacing.md,
|
|
44
|
+
backgroundColor: tokens.colors.background,
|
|
45
|
+
borderBottomWidth: 1,
|
|
46
|
+
borderBottomColor: tokens.colors.border,
|
|
47
|
+
zIndex: 10,
|
|
48
|
+
},
|
|
49
|
+
closeButton: {
|
|
50
|
+
padding: tokens.spacing.xs,
|
|
51
|
+
marginRight: tokens.spacing.sm,
|
|
52
|
+
},
|
|
53
|
+
titleContainer: {
|
|
54
|
+
flex: 1,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
},
|
|
57
|
+
title: {
|
|
58
|
+
fontSize: 18,
|
|
59
|
+
fontWeight: '700',
|
|
60
|
+
color: tokens.colors.textPrimary,
|
|
61
|
+
marginBottom: 4,
|
|
62
|
+
textAlign: 'center',
|
|
63
|
+
},
|
|
64
|
+
dateBadge: {
|
|
65
|
+
flexDirection: 'row',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: 4,
|
|
68
|
+
paddingHorizontal: 10,
|
|
69
|
+
paddingVertical: 4,
|
|
70
|
+
borderRadius: 12,
|
|
71
|
+
backgroundColor: tokens.colors.primary + '15',
|
|
72
|
+
},
|
|
73
|
+
dateText: {
|
|
74
|
+
fontSize: 12,
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
color: tokens.colors.primary,
|
|
77
|
+
},
|
|
78
|
+
placeholder: {
|
|
79
|
+
width: 40,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet, Image, Dimensions } from 'react-native';
|
|
4
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
|
|
6
|
+
interface DetailImageProps {
|
|
7
|
+
readonly uri: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { width } = Dimensions.get('window');
|
|
11
|
+
|
|
12
|
+
export const DetailImage: React.FC<DetailImageProps> = ({ uri }) => {
|
|
13
|
+
const tokens = useAppDesignTokens();
|
|
14
|
+
const styles = useStyles(tokens);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View style={styles.container}>
|
|
18
|
+
<View style={styles.frame}>
|
|
19
|
+
<Image source={{ uri }} style={styles.image} resizeMode="cover" />
|
|
20
|
+
</View>
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
26
|
+
container: {
|
|
27
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
28
|
+
marginVertical: tokens.spacing.lg,
|
|
29
|
+
},
|
|
30
|
+
frame: {
|
|
31
|
+
width: width - (tokens.spacing.lg * 2),
|
|
32
|
+
height: width - (tokens.spacing.lg * 2),
|
|
33
|
+
borderRadius: 24,
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
backgroundColor: tokens.colors.surface,
|
|
36
|
+
},
|
|
37
|
+
image: {
|
|
38
|
+
width: '100%',
|
|
39
|
+
height: '100%',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet } from 'react-native';
|
|
4
|
+
import { AtomicText, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
6
|
+
|
|
7
|
+
interface DetailStoryProps {
|
|
8
|
+
readonly story: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DetailStory: React.FC<DetailStoryProps> = ({ story }) => {
|
|
12
|
+
const tokens = useAppDesignTokens();
|
|
13
|
+
const styles = useStyles(tokens);
|
|
14
|
+
|
|
15
|
+
if (!story) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View style={styles.container}>
|
|
19
|
+
<LinearGradient
|
|
20
|
+
colors={[tokens.colors.primary + '15', tokens.colors.primary + '05']}
|
|
21
|
+
style={styles.gradient}
|
|
22
|
+
>
|
|
23
|
+
<AtomicText style={styles.quoteMark}>"</AtomicText>
|
|
24
|
+
<AtomicText style={styles.text}>{story}</AtomicText>
|
|
25
|
+
<View style={styles.quoteEndRow}>
|
|
26
|
+
<AtomicText style={[styles.quoteMark, styles.quoteEnd]}>"</AtomicText>
|
|
27
|
+
</View>
|
|
28
|
+
</LinearGradient>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
36
|
+
marginBottom: tokens.spacing.lg,
|
|
37
|
+
},
|
|
38
|
+
gradient: {
|
|
39
|
+
padding: tokens.spacing.lg,
|
|
40
|
+
borderRadius: 20,
|
|
41
|
+
borderWidth: 1,
|
|
42
|
+
borderColor: tokens.colors.primary + '20',
|
|
43
|
+
},
|
|
44
|
+
quoteMark: {
|
|
45
|
+
fontSize: 48,
|
|
46
|
+
lineHeight: 48,
|
|
47
|
+
color: tokens.colors.primary,
|
|
48
|
+
opacity: 0.4,
|
|
49
|
+
marginBottom: -16,
|
|
50
|
+
},
|
|
51
|
+
quoteEndRow: {
|
|
52
|
+
alignItems: 'flex-end',
|
|
53
|
+
marginTop: -16,
|
|
54
|
+
},
|
|
55
|
+
quoteEnd: {
|
|
56
|
+
marginBottom: 0,
|
|
57
|
+
},
|
|
58
|
+
text: {
|
|
59
|
+
fontSize: 16,
|
|
60
|
+
lineHeight: 26,
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
fontStyle: 'italic',
|
|
63
|
+
fontWeight: '500',
|
|
64
|
+
color: tokens.colors.textPrimary,
|
|
65
|
+
paddingBottom: 4,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
import { ImageGallery } from "@umituz/react-native-image";
|
|
3
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
4
|
+
|
|
5
|
+
interface CreationImageViewerProps {
|
|
6
|
+
readonly creations: Creation[];
|
|
7
|
+
readonly visible: boolean;
|
|
8
|
+
readonly index: number;
|
|
9
|
+
readonly enableEditing?: boolean;
|
|
10
|
+
readonly onDismiss: () => void;
|
|
11
|
+
readonly onIndexChange: (index: number) => void;
|
|
12
|
+
readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
|
|
13
|
+
readonly selectedCreationId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CreationImageViewer: React.FC<CreationImageViewerProps> = ({
|
|
17
|
+
creations,
|
|
18
|
+
visible,
|
|
19
|
+
index,
|
|
20
|
+
enableEditing = false,
|
|
21
|
+
onDismiss,
|
|
22
|
+
onIndexChange,
|
|
23
|
+
onImageEdit,
|
|
24
|
+
}) => {
|
|
25
|
+
const handleImageChange = useCallback(async (uri: string, idx: number) => {
|
|
26
|
+
const creation = creations[idx];
|
|
27
|
+
if (creation && onImageEdit) {
|
|
28
|
+
await onImageEdit(uri, creation.id);
|
|
29
|
+
}
|
|
30
|
+
}, [creations, onImageEdit]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ImageGallery
|
|
34
|
+
images={creations.map((c) => ({ uri: c.uri }))}
|
|
35
|
+
visible={visible}
|
|
36
|
+
index={index}
|
|
37
|
+
onDismiss={onDismiss}
|
|
38
|
+
onIndexChange={onIndexChange}
|
|
39
|
+
enableEditing={enableEditing}
|
|
40
|
+
onImageChange={onImageEdit ? handleImageChange : undefined}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationThumbnail Component
|
|
3
|
+
* Displays a single creation thumbnail
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useState } from "react";
|
|
7
|
+
import { Image, TouchableOpacity, StyleSheet, View } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
useAppDesignTokens,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
|
|
13
|
+
interface CreationThumbnailProps {
|
|
14
|
+
readonly uri: string;
|
|
15
|
+
readonly size?: number;
|
|
16
|
+
readonly onPress?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CreationThumbnail({
|
|
20
|
+
uri,
|
|
21
|
+
size = 72,
|
|
22
|
+
onPress,
|
|
23
|
+
}: CreationThumbnailProps) {
|
|
24
|
+
const tokens = useAppDesignTokens();
|
|
25
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
26
|
+
|
|
27
|
+
const styles = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
StyleSheet.create({
|
|
30
|
+
thumbnail: {
|
|
31
|
+
width: size,
|
|
32
|
+
height: size,
|
|
33
|
+
borderRadius: tokens.spacing.sm,
|
|
34
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
35
|
+
},
|
|
36
|
+
overlay: {
|
|
37
|
+
...StyleSheet.absoluteFillObject,
|
|
38
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
39
|
+
borderRadius: tokens.spacing.sm,
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
[tokens, size]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TouchableOpacity
|
|
49
|
+
onPress={onPress}
|
|
50
|
+
disabled={!onPress}
|
|
51
|
+
onPressIn={() => setIsPressed(true)}
|
|
52
|
+
onPressOut={() => setIsPressed(false)}
|
|
53
|
+
activeOpacity={1}
|
|
54
|
+
>
|
|
55
|
+
<Image source={{ uri }} style={styles.thumbnail} />
|
|
56
|
+
{isPressed && onPress && (
|
|
57
|
+
<View style={styles.overlay}>
|
|
58
|
+
<AtomicIcon name="eye" size="lg" color="textInverse" />
|
|
59
|
+
</View>
|
|
60
|
+
)}
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FlatList, RefreshControl, StyleSheet, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
5
|
+
import type { CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
6
|
+
import { CreationCard } from "./CreationCard";
|
|
7
|
+
|
|
8
|
+
interface CreationsGridProps {
|
|
9
|
+
readonly creations: Creation[];
|
|
10
|
+
readonly types: readonly CreationType[];
|
|
11
|
+
readonly isLoading: boolean;
|
|
12
|
+
readonly onRefresh: () => void;
|
|
13
|
+
readonly onView: (creation: Creation) => void;
|
|
14
|
+
readonly onShare: (creation: Creation) => void;
|
|
15
|
+
readonly onDelete: (creation: Creation) => void;
|
|
16
|
+
readonly contentContainerStyle?: ViewStyle;
|
|
17
|
+
readonly ListEmptyComponent?: React.ReactElement | null;
|
|
18
|
+
readonly ListHeaderComponent?: React.ComponentType<unknown> | React.ReactElement | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CreationsGrid: React.FC<CreationsGridProps> = ({
|
|
22
|
+
creations,
|
|
23
|
+
types,
|
|
24
|
+
isLoading,
|
|
25
|
+
onRefresh,
|
|
26
|
+
onView,
|
|
27
|
+
onShare,
|
|
28
|
+
onDelete,
|
|
29
|
+
contentContainerStyle,
|
|
30
|
+
ListEmptyComponent,
|
|
31
|
+
ListHeaderComponent,
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
const styles = useStyles(tokens);
|
|
35
|
+
|
|
36
|
+
const renderItem = ({ item }: { item: Creation }) => (
|
|
37
|
+
<CreationCard
|
|
38
|
+
creation={item}
|
|
39
|
+
types={types as CreationType[]}
|
|
40
|
+
onView={() => onView(item)}
|
|
41
|
+
onShare={() => onShare(item)}
|
|
42
|
+
onDelete={() => onDelete(item)}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<FlatList
|
|
48
|
+
data={creations}
|
|
49
|
+
renderItem={renderItem}
|
|
50
|
+
keyExtractor={(item) => item.id}
|
|
51
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
52
|
+
ListEmptyComponent={ListEmptyComponent}
|
|
53
|
+
contentContainerStyle={[
|
|
54
|
+
styles.list,
|
|
55
|
+
contentContainerStyle,
|
|
56
|
+
(!creations || creations.length === 0) && { flexGrow: 1 }
|
|
57
|
+
]}
|
|
58
|
+
showsVerticalScrollIndicator={false}
|
|
59
|
+
refreshControl={
|
|
60
|
+
<RefreshControl
|
|
61
|
+
refreshing={isLoading}
|
|
62
|
+
onRefresh={onRefresh}
|
|
63
|
+
tintColor={tokens.colors.primary}
|
|
64
|
+
/>
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
71
|
+
list: {
|
|
72
|
+
padding: tokens.spacing.md,
|
|
73
|
+
paddingBottom: 100, // Space for fab or bottom tab
|
|
74
|
+
},
|
|
75
|
+
});
|