@umituz/react-native-ai-generation-content 1.17.269 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.17.269",
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",
@@ -25,4 +25,9 @@ export interface ICreationsRepository {
25
25
  creationId: string,
26
26
  isFavorite: boolean,
27
27
  ): Promise<boolean>;
28
+ rate(
29
+ userId: string,
30
+ creationId: string,
31
+ rating: number,
32
+ ): Promise<boolean>;
28
33
  }
@@ -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 { useQuery } from "@tanstack/react-query";
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 useQuery<Creation[]>({
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 { useMutation, useQueryClient } from "@tanstack/react-query";
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
- const queryClient = useQueryClient();
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
- 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
- },
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 { View, ScrollView, StyleSheet } from 'react-native';
3
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
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 insets = useSafeAreaInsets();
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
- <View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
72
- <View style={{ paddingTop: insets.top }}>
73
- <DetailHeader onClose={onClose} />
74
- </View>
75
- <ScrollView
76
- style={styles.scrollView}
77
- contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
78
- showsVerticalScrollIndicator={false}
79
- >
80
- {isVideo ? (
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
- <DetailInfo title={title} date={date} />
92
+ <CreationRating
93
+ rating={creation.rating || 0}
94
+ onRate={handleRate}
95
+ />
87
96
 
88
- {story ? (
89
- <DetailStory story={story} />
90
- ) : null}
97
+ {story ? (
98
+ <DetailStory story={story} />
99
+ ) : null}
91
100
 
92
- <DetailActions
93
- onShare={() => onShare(creation)}
94
- onDelete={() => onDelete(creation)}
95
- onViewResult={onViewResult ? () => onViewResult(creation) : undefined}
96
- shareLabel={t("result.shareButton")}
97
- deleteLabel={t("common.delete")}
98
- viewResultLabel={onViewResult ? t("result.viewResult") : undefined}
99
- />
100
- </ScrollView>
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, { paddingTop: insets.top + tokens.spacing.md, backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
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, insets.top, tokens]);
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 <CreationDetailScreen creation={selectedCreation} config={config} onClose={() => setSelectedCreation(null)} onShare={handleShare} onDelete={handleDelete} onViewResult={onViewResult} t={t} />;
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
- <View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
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={[styles.listContent, { paddingBottom: insets.bottom + 100 }, (!filters.filtered || filters.filtered.length === 0) && styles.emptyContent]}
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
- </View>
185
+ </ScreenLayout>
172
186
  );
173
187
  }
174
188