@umituz/react-native-settings 4.19.4 → 4.19.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "4.19.4",
3
+ "version": "4.19.6",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, and rating",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -8,3 +8,4 @@ export * from './presentation/components/AboutContent';
8
8
  export * from './presentation/components/AboutSection';
9
9
  export * from './presentation/components/AboutSettingItem';
10
10
  export * from './presentation/components/AboutHeader';
11
+ export * from './domain/entities/AppInfo';
@@ -7,10 +7,17 @@ export interface FAQItem {
7
7
  question: string;
8
8
  answer: string;
9
9
  categoryId?: string;
10
+ tags?: readonly string[];
11
+ featured?: boolean;
12
+ order?: number;
13
+ metadata?: Record<string, unknown>;
10
14
  }
11
15
 
12
16
  export interface FAQCategory {
13
17
  id: string;
14
18
  title: string;
15
19
  items: FAQItem[];
20
+ description?: string;
21
+ icon?: string;
22
+ order?: number;
16
23
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * FAQ Search Service
3
- * Handles searching FAQ items
3
+ * Handles searching FAQ items with advanced features
4
4
  */
5
5
 
6
6
  import { FAQItem, FAQCategory } from "../entities/FAQEntity";
@@ -11,10 +11,15 @@ export class FAQSearchService {
11
11
 
12
12
  const normalizedQuery = query.toLowerCase().trim();
13
13
 
14
- return items.filter(item =>
15
- item.question.toLowerCase().includes(normalizedQuery) ||
16
- item.answer.toLowerCase().includes(normalizedQuery)
17
- );
14
+ return items.filter(item => {
15
+ const questionMatch = item.question.toLowerCase().includes(normalizedQuery);
16
+ const answerMatch = item.answer.toLowerCase().includes(normalizedQuery);
17
+ const tagMatch = item.tags?.some(tag =>
18
+ tag.toLowerCase().includes(normalizedQuery)
19
+ ) || false;
20
+
21
+ return questionMatch || answerMatch || tagMatch;
22
+ });
18
23
  }
19
24
 
20
25
  static searchCategories(query: string, categories: FAQCategory[]): FAQCategory[] {
@@ -25,12 +30,30 @@ export class FAQSearchService {
25
30
  return categories
26
31
  .map(category => ({
27
32
  ...category,
28
- items: category.items.filter(item =>
29
- item.question.toLowerCase().includes(normalizedQuery) ||
30
- item.answer.toLowerCase().includes(normalizedQuery) ||
31
- category.title.toLowerCase().includes(normalizedQuery)
32
- )
33
+ items: category.items.filter(item => {
34
+ const questionMatch = item.question.toLowerCase().includes(normalizedQuery);
35
+ const answerMatch = item.answer.toLowerCase().includes(normalizedQuery);
36
+ const categoryMatch = category.title.toLowerCase().includes(normalizedQuery);
37
+ const categoryDescMatch = category.description?.toLowerCase().includes(normalizedQuery) || false;
38
+ const tagMatch = item.tags?.some(tag =>
39
+ tag.toLowerCase().includes(normalizedQuery)
40
+ ) || false;
41
+
42
+ return questionMatch || answerMatch || categoryMatch || categoryDescMatch || tagMatch;
43
+ })
33
44
  }))
34
45
  .filter(category => category.items.length > 0);
35
46
  }
47
+
48
+ static getFeaturedItems(items: FAQItem[]): FAQItem[] {
49
+ return items.filter(item => item.featured === true);
50
+ }
51
+
52
+ static sortByOrder(items: FAQItem[]): FAQItem[] {
53
+ return [...items].sort((a, b) => (a.order || 0) - (b.order || 0));
54
+ }
55
+
56
+ static sortCategoriesByOrder(categories: FAQCategory[]): FAQCategory[] {
57
+ return [...categories].sort((a, b) => (a.order || 0) - (b.order || 0));
58
+ }
36
59
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Video Tutorials Domain
3
+ */
4
+
5
+ export * from "./types";
6
+ export * from "./presentation/screens/VideoTutorialsScreen";
7
+ export * from "./presentation/hooks";
8
+ export * from "./infrastructure/services/video-tutorial.service";
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Video Tutorial Service
3
+ * Single Responsibility: Handle video tutorial data operations
4
+ */
5
+
6
+ import {
7
+ collection,
8
+ getDocs,
9
+ query,
10
+ orderBy,
11
+ where,
12
+ limit,
13
+ } from "firebase/firestore";
14
+ import type { Firestore } from "firebase/firestore";
15
+ import type {
16
+ VideoTutorial,
17
+ VideoTutorialCategory,
18
+ VideoTutorialFilters,
19
+ } from "../../types";
20
+
21
+ export interface VideoTutorialServiceConfig {
22
+ db?: Firestore;
23
+ collectionName?: string;
24
+ }
25
+
26
+ class VideoTutorialService {
27
+ private config: VideoTutorialServiceConfig = {
28
+ collectionName: "video_tutorials",
29
+ };
30
+
31
+ initialize(config: VideoTutorialServiceConfig) {
32
+ this.config = { ...this.config, ...config };
33
+ }
34
+
35
+ private get db(): Firestore {
36
+ if (!this.config.db) {
37
+ throw new Error("VideoTutorialService: Firestore not initialized. Call initialize({ db }) first.");
38
+ }
39
+ return this.config.db;
40
+ }
41
+
42
+ private get collectionName(): string {
43
+ return this.config.collectionName || "video_tutorials";
44
+ }
45
+
46
+ async getAllTutorials(
47
+ filters?: VideoTutorialFilters,
48
+ ): Promise<VideoTutorial[]> {
49
+ const constraints = [];
50
+
51
+ if (filters?.category) {
52
+ constraints.push(where("category", "==", filters.category));
53
+ }
54
+
55
+ if (filters?.difficulty) {
56
+ constraints.push(where("difficulty", "==", filters.difficulty));
57
+ }
58
+
59
+ if (filters?.featured !== undefined) {
60
+ constraints.push(where("featured", "==", filters.featured));
61
+ }
62
+
63
+ constraints.push(orderBy("createdAt", "desc"));
64
+
65
+ const q = query(collection(this.db, this.collectionName), ...constraints);
66
+ const snapshot = await getDocs(q);
67
+
68
+ return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
69
+ }
70
+
71
+ async getFeaturedTutorials(maxCount: number = 5): Promise<VideoTutorial[]> {
72
+ const q = query(
73
+ collection(this.db, this.collectionName),
74
+ where("featured", "==", true),
75
+ orderBy("createdAt", "desc"),
76
+ limit(maxCount),
77
+ );
78
+
79
+ const snapshot = await getDocs(q);
80
+ return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
81
+ }
82
+
83
+ async getTutorialsByCategory(
84
+ category: VideoTutorialCategory,
85
+ ): Promise<VideoTutorial[]> {
86
+ const q = query(
87
+ collection(this.db, this.collectionName),
88
+ where("category", "==", category),
89
+ orderBy("createdAt", "desc"),
90
+ );
91
+
92
+ const snapshot = await getDocs(q);
93
+ return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
94
+ }
95
+
96
+ private mapDocumentToTutorial(doc: any): VideoTutorial {
97
+ const data = doc.data();
98
+ return {
99
+ id: doc.id,
100
+ title: data.title || "",
101
+ description: data.description || "",
102
+ videoUrl: data.videoUrl || "",
103
+ thumbnailUrl: data.thumbnailUrl || "",
104
+ duration: data.duration || 0,
105
+ category: data.category || "getting-started",
106
+ difficulty: data.difficulty || "beginner",
107
+ featured: data.featured || false,
108
+ tags: data.tags || [],
109
+ createdAt: data.createdAt?.toDate() || new Date(),
110
+ updatedAt: data.updatedAt?.toDate() || new Date(),
111
+ viewCount: data.viewCount || 0,
112
+ };
113
+ }
114
+ }
115
+
116
+ export const videoTutorialService = new VideoTutorialService();
117
+
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Video Tutorial Card Component
3
+ * Single Responsibility: Display individual video tutorial card
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, Image, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import type { VideoTutorial } from "../../types";
10
+
11
+ interface VideoTutorialCardProps {
12
+ readonly tutorial: VideoTutorial;
13
+ readonly onPress: () => void;
14
+ readonly horizontal?: boolean;
15
+ }
16
+
17
+ export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
18
+ tutorial,
19
+ onPress,
20
+ horizontal = false,
21
+ }) => {
22
+ const tokens = useAppDesignTokens();
23
+
24
+ const formatDuration = (seconds: number): string => {
25
+ const minutes = Math.floor(seconds / 60);
26
+ const remainingSeconds = seconds % 60;
27
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
28
+ };
29
+
30
+ return (
31
+ <TouchableOpacity
32
+ style={[
33
+ styles.container,
34
+ {
35
+ backgroundColor: tokens.colors.surface,
36
+ borderColor: tokens.colors.border,
37
+ },
38
+ horizontal && styles.horizontalContainer,
39
+ ]}
40
+ onPress={onPress}
41
+ activeOpacity={0.7}
42
+ >
43
+ <View style={styles.imageContainer}>
44
+ <Image
45
+ source={{ uri: tutorial.thumbnailUrl }}
46
+ style={[styles.thumbnail, horizontal && styles.horizontalThumbnail]}
47
+ resizeMode="cover"
48
+ />
49
+ <View
50
+ style={[styles.durationBadge, { backgroundColor: "rgba(0,0,0,0.7)" }]}
51
+ >
52
+ <Text style={styles.durationText}>
53
+ {formatDuration(tutorial.duration)}
54
+ </Text>
55
+ </View>
56
+ {tutorial.featured && (
57
+ <View
58
+ style={[
59
+ styles.featuredBadge,
60
+ { backgroundColor: tokens.colors.primary },
61
+ ]}
62
+ >
63
+ <Text style={[styles.featuredText, { color: "#FFFFFF" }]}>
64
+ Featured
65
+ </Text>
66
+ </View>
67
+ )}
68
+ </View>
69
+
70
+ <View style={styles.content}>
71
+ <Text
72
+ style={[
73
+ styles.title,
74
+ { color: tokens.colors.textPrimary },
75
+ horizontal && styles.horizontalTitle,
76
+ ]}
77
+ numberOfLines={2}
78
+ >
79
+ {tutorial.title}
80
+ </Text>
81
+
82
+ <Text
83
+ style={[
84
+ styles.description,
85
+ { color: tokens.colors.textSecondary },
86
+ horizontal && styles.horizontalDescription,
87
+ ]}
88
+ numberOfLines={horizontal ? 2 : 3}
89
+ >
90
+ {tutorial.description}
91
+ </Text>
92
+
93
+ <View style={styles.metadata}>
94
+ <Text
95
+ style={[styles.category, { color: tokens.colors.textTertiary }]}
96
+ >
97
+ {tutorial.category.replace("-", " ")}
98
+ </Text>
99
+ <Text
100
+ style={[styles.difficulty, { color: tokens.colors.textSecondary }]}
101
+ >
102
+ {tutorial.difficulty}
103
+ </Text>
104
+ </View>
105
+ </View>
106
+ </TouchableOpacity>
107
+ );
108
+ };
109
+
110
+ const styles = StyleSheet.create({
111
+ container: {
112
+ borderRadius: 12,
113
+ borderWidth: 1,
114
+ marginBottom: 12,
115
+ overflow: "hidden",
116
+ },
117
+ horizontalContainer: {
118
+ width: 280,
119
+ marginRight: 12,
120
+ marginBottom: 0,
121
+ },
122
+ imageContainer: {
123
+ position: "relative",
124
+ },
125
+ thumbnail: {
126
+ width: "100%",
127
+ height: 180,
128
+ },
129
+ horizontalThumbnail: {
130
+ height: 140,
131
+ },
132
+ durationBadge: {
133
+ position: "absolute",
134
+ bottom: 8,
135
+ right: 8,
136
+ paddingHorizontal: 6,
137
+ paddingVertical: 2,
138
+ borderRadius: 4,
139
+ },
140
+ durationText: {
141
+ color: "white",
142
+ fontSize: 12,
143
+ fontWeight: "500",
144
+ },
145
+ featuredBadge: {
146
+ position: "absolute",
147
+ top: 8,
148
+ left: 8,
149
+ paddingHorizontal: 8,
150
+ paddingVertical: 4,
151
+ borderRadius: 4,
152
+ },
153
+ featuredText: {
154
+ fontSize: 11,
155
+ fontWeight: "600",
156
+ },
157
+ content: {
158
+ padding: 12,
159
+ },
160
+ title: {
161
+ fontSize: 16,
162
+ fontWeight: "600",
163
+ marginBottom: 6,
164
+ },
165
+ horizontalTitle: {
166
+ fontSize: 14,
167
+ },
168
+ description: {
169
+ fontSize: 14,
170
+ lineHeight: 20,
171
+ marginBottom: 8,
172
+ },
173
+ horizontalDescription: {
174
+ fontSize: 12,
175
+ lineHeight: 16,
176
+ marginBottom: 6,
177
+ },
178
+ metadata: {
179
+ flexDirection: "row",
180
+ justifyContent: "space-between",
181
+ alignItems: "center",
182
+ },
183
+ category: {
184
+ fontSize: 12,
185
+ textTransform: "capitalize",
186
+ },
187
+ difficulty: {
188
+ fontSize: 12,
189
+ textTransform: "capitalize",
190
+ },
191
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Video Tutorial Hooks
3
+ * Single Responsibility: Provide React hooks for video tutorials
4
+ */
5
+
6
+ import { useQuery } from "@tanstack/react-query";
7
+ import { videoTutorialService } from "../../infrastructure/services/video-tutorial.service";
8
+ import type {
9
+ VideoTutorial,
10
+ VideoTutorialCategory,
11
+ VideoTutorialFilters,
12
+ } from "../../types";
13
+
14
+ export function useVideoTutorials(filters?: VideoTutorialFilters) {
15
+ return useQuery({
16
+ queryKey: ["video-tutorials", filters],
17
+ queryFn: () => videoTutorialService.getAllTutorials(filters),
18
+ staleTime: 5 * 60 * 1000, // 5 minutes
19
+ });
20
+ }
21
+
22
+ export function useFeaturedTutorials(maxCount: number = 5) {
23
+ return useQuery({
24
+ queryKey: ["featured-tutorials", maxCount],
25
+ queryFn: () => videoTutorialService.getFeaturedTutorials(maxCount),
26
+ staleTime: 10 * 60 * 1000, // 10 minutes
27
+ });
28
+ }
29
+
30
+ export function useTutorialsByCategory(category: VideoTutorialCategory) {
31
+ return useQuery({
32
+ queryKey: ["tutorials-by-category", category],
33
+ queryFn: () => videoTutorialService.getTutorialsByCategory(category),
34
+ staleTime: 5 * 60 * 1000, // 5 minutes
35
+ });
36
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Video Tutorials Screen
3
+ * Single Responsibility: Display video tutorials list
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, FlatList, StyleSheet } from "react-native";
8
+ import {
9
+ useAppDesignTokens,
10
+ ScreenLayout,
11
+ AtomicSpinner,
12
+ AtomicText,
13
+ } from "@umituz/react-native-design-system";
14
+ import type { VideoTutorial } from "../../types";
15
+ import { VideoTutorialCard } from "../components/VideoTutorialCard";
16
+ import { useVideoTutorials, useFeaturedTutorials } from "../hooks";
17
+
18
+ export interface VideoTutorialsScreenProps {
19
+ /**
20
+ * Title of the screen
21
+ */
22
+ title?: string;
23
+ /**
24
+ * Title for the featured tutorials section
25
+ */
26
+ featuredTitle?: string;
27
+ /**
28
+ * Title for all tutorials section
29
+ */
30
+ allTutorialsTitle?: string;
31
+ /**
32
+ * Error message to show when tutorials fail to load
33
+ */
34
+ errorLoadingMessage?: string;
35
+ /**
36
+ * Maximum number of featured tutorials to show
37
+ */
38
+ maxFeaturedCount?: number;
39
+ /**
40
+ * Callback when a tutorial is pressed
41
+ */
42
+ onTutorialPress?: (tutorialId: string) => void;
43
+ /**
44
+ * Optional manual override for loading state
45
+ */
46
+ customIsLoading?: boolean;
47
+ /**
48
+ * Optional manual override for all tutorials data
49
+ */
50
+ customAllTutorials?: VideoTutorial[];
51
+ /**
52
+ * Optional manual override for featured tutorials data
53
+ */
54
+ customFeaturedTutorials?: VideoTutorial[];
55
+ }
56
+
57
+ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
58
+ React.memo(
59
+ ({
60
+ title = "Video Tutorials",
61
+ featuredTitle = "Featured",
62
+ allTutorialsTitle = "All Tutorials",
63
+ errorLoadingMessage = "Failed to load tutorials.",
64
+ maxFeaturedCount = 3,
65
+ onTutorialPress,
66
+ customIsLoading,
67
+ customAllTutorials,
68
+ customFeaturedTutorials,
69
+ }) => {
70
+ const tokens = useAppDesignTokens();
71
+
72
+ const featuredQuery = useFeaturedTutorials(maxFeaturedCount);
73
+ const allQuery = useVideoTutorials();
74
+
75
+ const isLoading =
76
+ customIsLoading !== undefined
77
+ ? customIsLoading
78
+ : featuredQuery.isLoading || allQuery.isLoading;
79
+ const error = featuredQuery.error || allQuery.error;
80
+
81
+ const featuredTutorials =
82
+ customFeaturedTutorials || featuredQuery.data || [];
83
+ const allTutorials = customAllTutorials || allQuery.data || [];
84
+
85
+ const handleTutorialPress = React.useCallback(
86
+ (tutorialId: string) => {
87
+ if (onTutorialPress) {
88
+ onTutorialPress(tutorialId);
89
+ } else if (__DEV__) {
90
+ // eslint-disable-next-line no-console
91
+ console.log("VideoTutorialsScreen: No onTutorialPress handler", {
92
+ tutorialId,
93
+ });
94
+ }
95
+ },
96
+ [onTutorialPress],
97
+ );
98
+
99
+ const renderTutorialItem = React.useCallback(
100
+ ({ item }: { item: VideoTutorial }) => (
101
+ <VideoTutorialCard
102
+ tutorial={item}
103
+ onPress={() => handleTutorialPress(item.id)}
104
+ />
105
+ ),
106
+ [handleTutorialPress],
107
+ );
108
+
109
+ if (isLoading) {
110
+ return <AtomicSpinner size="lg" fullContainer />;
111
+ }
112
+
113
+ if (error) {
114
+ return (
115
+ <View style={styles.errorContainer}>
116
+ <AtomicText color="error" type="bodyLarge">
117
+ {errorLoadingMessage}
118
+ </AtomicText>
119
+ </View>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <ScreenLayout scrollable={false} edges={["top", "bottom"]}>
125
+ <Text style={[styles.title, { color: tokens.colors.textPrimary }]}>
126
+ {title}
127
+ </Text>
128
+
129
+ {featuredTutorials && featuredTutorials.length > 0 && (
130
+ <View style={styles.section}>
131
+ <Text
132
+ style={[
133
+ styles.sectionTitle,
134
+ { color: tokens.colors.textSecondary },
135
+ ]}
136
+ >
137
+ {featuredTitle}
138
+ </Text>
139
+ <FlatList
140
+ data={featuredTutorials}
141
+ renderItem={renderTutorialItem}
142
+ keyExtractor={(item: VideoTutorial) => item.id}
143
+ horizontal
144
+ showsHorizontalScrollIndicator={false}
145
+ contentContainerStyle={styles.horizontalList}
146
+ />
147
+ </View>
148
+ )}
149
+
150
+ <View style={styles.section}>
151
+ <Text
152
+ style={[
153
+ styles.sectionTitle,
154
+ { color: tokens.colors.textSecondary },
155
+ ]}
156
+ >
157
+ {allTutorialsTitle}
158
+ </Text>
159
+ <FlatList
160
+ data={allTutorials}
161
+ renderItem={renderTutorialItem}
162
+ keyExtractor={(item: VideoTutorial) => item.id}
163
+ showsVerticalScrollIndicator={false}
164
+ contentContainerStyle={styles.verticalList}
165
+ />
166
+ </View>
167
+ </ScreenLayout>
168
+ );
169
+ },
170
+ );
171
+
172
+ const styles = StyleSheet.create({
173
+ title: {
174
+ fontSize: 24,
175
+ fontWeight: "600",
176
+ marginBottom: 24,
177
+ },
178
+ section: {
179
+ marginBottom: 24,
180
+ },
181
+ sectionTitle: {
182
+ fontSize: 18,
183
+ fontWeight: "500",
184
+ marginBottom: 12,
185
+ },
186
+ horizontalList: {
187
+ paddingRight: 16,
188
+ },
189
+ verticalList: {
190
+ paddingBottom: 16,
191
+ },
192
+ errorContainer: {
193
+ flex: 1,
194
+ justifyContent: "center",
195
+ alignItems: "center",
196
+ padding: 20,
197
+ },
198
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Video Tutorial Types
3
+ * Single Responsibility: Define type definitions for video tutorials
4
+ */
5
+
6
+ export interface VideoTutorial {
7
+ readonly id: string;
8
+ readonly title: string;
9
+ readonly description: string;
10
+ readonly videoUrl: string;
11
+ readonly thumbnailUrl: string;
12
+ readonly duration: number; // in seconds
13
+ readonly category: VideoTutorialCategory;
14
+ readonly difficulty: "beginner" | "intermediate" | "advanced";
15
+ readonly featured: boolean;
16
+ readonly tags: readonly string[];
17
+ readonly createdAt: Date;
18
+ readonly updatedAt: Date;
19
+ readonly viewCount: number;
20
+ }
21
+
22
+ export type VideoTutorialCategory =
23
+ | "getting-started"
24
+ | "ai-generation"
25
+ | "editing"
26
+ | "effects"
27
+ | "exporting"
28
+ | "tips-tricks"
29
+ | "troubleshooting";
30
+
31
+ export interface VideoTutorialFilters {
32
+ readonly category?: VideoTutorialCategory;
33
+ readonly difficulty?: VideoTutorial["difficulty"];
34
+ readonly featured?: boolean;
35
+ readonly tags?: readonly string[];
36
+ }
package/src/index.ts CHANGED
@@ -115,7 +115,10 @@ export * from './domains/feedback';
115
115
  export * from './domains/faqs';
116
116
 
117
117
  // Rating Domain - Star ratings, reviews, statistics
118
- export * from './domains/rating';
118
+ export * from "./domains/rating";
119
+
120
+ // Video Tutorials Domain - Learning resources, tutorials
121
+ export * from "./domains/video-tutorials";
119
122
 
120
123
  // =============================================================================
121
124
  // PRESENTATION LAYER - Re-exports from Dependencies