@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 +1 -1
- package/src/domains/about/index.ts +1 -0
- package/src/domains/faqs/domain/entities/FAQEntity.ts +7 -0
- package/src/domains/faqs/domain/services/FAQSearchService.ts +33 -10
- package/src/domains/video-tutorials/index.ts +8 -0
- package/src/domains/video-tutorials/infrastructure/services/video-tutorial.service.ts +117 -0
- package/src/domains/video-tutorials/presentation/components/VideoTutorialCard.tsx +191 -0
- package/src/domains/video-tutorials/presentation/hooks/index.ts +36 -0
- package/src/domains/video-tutorials/presentation/screens/VideoTutorialsScreen.tsx +198 -0
- package/src/domains/video-tutorials/types/index.ts +36 -0
- package/src/index.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.19.
|
|
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,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
|
|
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
|