@umituz/react-native-settings 4.19.5 → 4.20.0
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 +24 -22
- package/src/domains/about/presentation/components/AboutContent.tsx +2 -2
- package/src/domains/about/presentation/components/AboutHeader.tsx +2 -2
- package/src/domains/about/presentation/components/AboutSettingItem.tsx +2 -2
- package/src/domains/about/presentation/screens/AboutScreen.tsx +2 -2
- package/src/domains/appearance/presentation/components/ColorPicker.tsx +3 -3
- package/src/domains/appearance/presentation/components/ThemeOption.tsx +3 -3
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +3 -3
- package/src/domains/faqs/domain/entities/FAQEntity.ts +7 -0
- package/src/domains/faqs/domain/services/FAQSearchService.ts +33 -10
- package/src/domains/faqs/presentation/components/FAQCategory.tsx +17 -8
- package/src/domains/faqs/presentation/components/FAQEmptyState.tsx +2 -2
- package/src/domains/faqs/presentation/components/FAQItem.tsx +36 -21
- package/src/domains/faqs/presentation/components/FAQSearchBar.tsx +12 -11
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +37 -27
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +2 -2
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +2 -2
- package/src/domains/legal/presentation/screens/LegalScreen.tsx +2 -2
- package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +2 -2
- package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +2 -2
- 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/src/presentation/components/DevSettingsSection.tsx +2 -2
- package/src/presentation/components/SettingItem.tsx +2 -2
- package/src/presentation/components/SettingsErrorBoundary.tsx +2 -2
- package/src/presentation/components/SettingsFooter.tsx +2 -2
- package/src/presentation/components/SettingsItemCard.tsx +2 -2
- package/src/presentation/components/SettingsSection.tsx +2 -2
- package/src/presentation/navigation/SettingsStackNavigator.tsx +2 -2
- package/src/presentation/screens/SettingsScreen.tsx +2 -2
- package/src/presentation/screens/components/SettingsContent.tsx +2 -2
- package/src/presentation/screens/components/SettingsHeader.tsx +2 -2
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useMemo } from 'react';
|
|
8
|
-
import { View, ScrollView, StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
|
9
|
-
import {
|
|
8
|
+
import { View, ScrollView, StyleSheet, ViewStyle, TextStyle, useWindowDimensions } from 'react-native';
|
|
9
|
+
import { useAppDesignTokens, AtomicText, ScreenLayout, getContentMaxWidth } from '@umituz/react-native-design-system';
|
|
10
10
|
import { FAQCategory } from '../../domain/entities/FAQEntity';
|
|
11
11
|
import { useFAQSearch } from '../hooks/useFAQSearch';
|
|
12
12
|
import { useFAQExpansion } from '../hooks/useFAQExpansion';
|
|
@@ -45,9 +45,10 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
45
45
|
renderHeader,
|
|
46
46
|
styles: customStyles,
|
|
47
47
|
}) => {
|
|
48
|
-
const tokens =
|
|
49
|
-
const {
|
|
50
|
-
|
|
48
|
+
const tokens = useAppDesignTokens();
|
|
49
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
50
|
+
const contentMaxWidth = useMemo(() => getContentMaxWidth(windowWidth), [windowWidth]);
|
|
51
|
+
const { searchQuery, setSearchQuery, filteredCategories, hasResults } = useFAQSearch(categories);
|
|
51
52
|
const { isExpanded, toggleExpansion } = useFAQExpansion();
|
|
52
53
|
|
|
53
54
|
const styles = useMemo(
|
|
@@ -55,12 +56,9 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
55
56
|
StyleSheet.create({
|
|
56
57
|
container: {
|
|
57
58
|
flex: 1,
|
|
58
|
-
backgroundColor: tokens.colors.backgroundPrimary,
|
|
59
59
|
},
|
|
60
60
|
header: {
|
|
61
61
|
padding: tokens.spacing.md,
|
|
62
|
-
borderBottomWidth: 1,
|
|
63
|
-
borderBottomColor: tokens.colors.border,
|
|
64
62
|
},
|
|
65
63
|
content: {
|
|
66
64
|
flex: 1,
|
|
@@ -81,12 +79,13 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
return (
|
|
84
|
-
<
|
|
85
|
-
style={[styles.content, customStyles?.content]}
|
|
86
|
-
showsVerticalScrollIndicator={false}
|
|
87
|
-
>
|
|
82
|
+
<View style={{ flex: 1 }}>
|
|
88
83
|
<View style={[styles.header, customStyles?.header]}>
|
|
89
|
-
<AtomicText
|
|
84
|
+
<AtomicText
|
|
85
|
+
type="headlineMedium"
|
|
86
|
+
color="textPrimary"
|
|
87
|
+
style={{ marginBottom: tokens.spacing.md, fontWeight: '700' }}
|
|
88
|
+
>
|
|
90
89
|
{headerTitle}
|
|
91
90
|
</AtomicText>
|
|
92
91
|
<FAQSearchBar
|
|
@@ -97,30 +96,41 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
97
96
|
/>
|
|
98
97
|
</View>
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
<ScrollView
|
|
100
|
+
style={[styles.content, customStyles?.content]}
|
|
101
|
+
contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
|
|
102
|
+
showsVerticalScrollIndicator={false}
|
|
103
|
+
>
|
|
104
|
+
{filteredCategories.map((category) => (
|
|
105
|
+
<FAQCategoryComponent
|
|
106
|
+
key={category.id}
|
|
107
|
+
category={category}
|
|
108
|
+
isExpanded={isExpanded}
|
|
109
|
+
onToggleItem={toggleExpansion}
|
|
110
|
+
styles={customStyles?.category}
|
|
111
|
+
/>
|
|
112
|
+
))}
|
|
113
|
+
<View style={{ height: tokens.spacing.xl * 2 }} />
|
|
114
|
+
</ScrollView>
|
|
115
|
+
</View>
|
|
110
116
|
);
|
|
111
117
|
};
|
|
112
118
|
|
|
113
119
|
if (renderHeader) {
|
|
114
120
|
return (
|
|
115
|
-
<View style={
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
<View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
|
|
122
|
+
<View style={[styles.container, customStyles?.container]}>
|
|
123
|
+
<View style={{ alignSelf: 'center', width: '100%', maxWidth: contentMaxWidth }}>
|
|
124
|
+
{renderHeader({ onBack: onBack || (() => { }) })}
|
|
125
|
+
</View>
|
|
126
|
+
{renderContent()}
|
|
127
|
+
</View>
|
|
118
128
|
</View>
|
|
119
129
|
);
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
return (
|
|
123
|
-
<ScreenLayout edges={['bottom']} scrollable={false}>
|
|
133
|
+
<ScreenLayout edges={['bottom']} scrollable={false} maxWidth={contentMaxWidth}>
|
|
124
134
|
<View style={[styles.container, customStyles?.container]}>
|
|
125
135
|
{renderContent()}
|
|
126
136
|
</View>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { useState } from "react";
|
|
7
7
|
import { View, StyleSheet, TouchableOpacity, ScrollView, TextInput } from "react-native";
|
|
8
|
-
import {
|
|
8
|
+
import { useAppDesignTokens, AtomicText, AtomicButton, AtomicIcon } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
10
10
|
import { useFeedbackForm } from "../hooks/useFeedbackForm";
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
|
29
29
|
initialType,
|
|
30
30
|
isSubmitting = false,
|
|
31
31
|
}) => {
|
|
32
|
-
const tokens =
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
33
|
const [selectedType, setSelectedType] = useState<FeedbackType>(initialType || texts.feedbackTypes[0].type);
|
|
34
34
|
const [rating, setRating] = useState<FeedbackRating>(5);
|
|
35
35
|
const [description, setDescription] = useState("");
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform } from "react-native";
|
|
8
8
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
9
|
-
import {
|
|
9
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon, BaseModal } from "@umituz/react-native-design-system";
|
|
10
10
|
import { FeedbackForm } from "./FeedbackForm";
|
|
11
11
|
import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
31
31
|
subtitle,
|
|
32
32
|
texts,
|
|
33
33
|
}) => {
|
|
34
|
-
const tokens =
|
|
34
|
+
const tokens = useAppDesignTokens();
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<BaseModal visible={visible} onClose={onClose}>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from "react";
|
|
11
11
|
import { View, StyleSheet } from "react-native";
|
|
12
|
-
import {
|
|
12
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
13
13
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
14
14
|
import { ScreenLayout } from "@umituz/react-native-design-system";
|
|
15
15
|
import { LegalItem } from "../components/LegalItem";
|
|
@@ -93,7 +93,7 @@ export const LegalScreen: React.FC<LegalScreenProps> = React.memo(({
|
|
|
93
93
|
eulaUrl,
|
|
94
94
|
testID = "legal-screen",
|
|
95
95
|
}) => {
|
|
96
|
-
const tokens =
|
|
96
|
+
const tokens = useAppDesignTokens();
|
|
97
97
|
|
|
98
98
|
// Memoize styles to prevent recreation on every render
|
|
99
99
|
const styles = React.useMemo(() => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
8
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
9
|
-
import {
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
10
|
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
|
|
11
11
|
import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
|
|
12
12
|
import { ContentValidationService } from "../../domain/services/ContentValidationService";
|
|
@@ -54,7 +54,7 @@ export const PrivacyPolicyScreen: React.FC<PrivacyPolicyScreenProps> = React.mem
|
|
|
54
54
|
onUrlPress,
|
|
55
55
|
testID = "privacy-policy-screen",
|
|
56
56
|
}) => {
|
|
57
|
-
const tokens =
|
|
57
|
+
const tokens = useAppDesignTokens();
|
|
58
58
|
const insets = useSafeAreaInsets();
|
|
59
59
|
|
|
60
60
|
// Validate required props
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
8
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
9
|
-
import {
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
10
|
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
|
|
11
11
|
import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
|
|
12
12
|
import { ContentValidationService } from "../../domain/services/ContentValidationService";
|
|
@@ -54,7 +54,7 @@ export const TermsOfServiceScreen: React.FC<TermsOfServiceScreenProps> = React.m
|
|
|
54
54
|
onUrlPress,
|
|
55
55
|
testID = "terms-of-service-screen",
|
|
56
56
|
}) => {
|
|
57
|
-
const tokens =
|
|
57
|
+
const tokens = useAppDesignTokens();
|
|
58
58
|
const insets = useSafeAreaInsets();
|
|
59
59
|
|
|
60
60
|
// Validate required props
|
|
@@ -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, QueryConstraint } 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: QueryConstraint[] = [];
|
|
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
|
+
}
|