@umituz/react-native-settings 5.3.38 → 5.3.40
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/dist/domains/feedback/presentation/components/FeedbackForm.styles.d.ts +15 -1
- package/package.json +1 -1
- package/src/domains/feedback/domain/entities/FeatureRequestEntity.ts +48 -0
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/components/FeedbackForm.styles.ts +26 -12
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +1 -1
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +241 -179
- package/src/presentation/hooks/useSettingsScreenConfig.ts +13 -2
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +17 -1
- package/src/presentation/screens/types/UserFeatureConfig.ts +3 -0
|
@@ -16,11 +16,21 @@ export declare const getFeedbackFormStyles: (_tokens: ReturnType<typeof useAppDe
|
|
|
16
16
|
paddingVertical: number;
|
|
17
17
|
borderRadius: number;
|
|
18
18
|
borderWidth: number;
|
|
19
|
-
|
|
19
|
+
marginRight: number;
|
|
20
20
|
};
|
|
21
21
|
ratingContainer: {
|
|
22
22
|
alignItems: "center";
|
|
23
|
+
marginVertical: number;
|
|
24
|
+
paddingVertical: number;
|
|
25
|
+
backgroundColor: string;
|
|
26
|
+
borderRadius: number;
|
|
27
|
+
};
|
|
28
|
+
ratingLabel: {
|
|
29
|
+
fontSize: number;
|
|
30
|
+
fontWeight: "900";
|
|
31
|
+
letterSpacing: number;
|
|
23
32
|
marginBottom: number;
|
|
33
|
+
textTransform: "uppercase";
|
|
24
34
|
};
|
|
25
35
|
stars: {
|
|
26
36
|
flexDirection: "row";
|
|
@@ -38,11 +48,15 @@ export declare const getFeedbackFormStyles: (_tokens: ReturnType<typeof useAppDe
|
|
|
38
48
|
borderWidth: number;
|
|
39
49
|
borderRadius: number;
|
|
40
50
|
padding: number;
|
|
51
|
+
fontSize: number;
|
|
41
52
|
};
|
|
42
53
|
errorText: {
|
|
43
54
|
marginTop: number;
|
|
55
|
+
fontWeight: "600";
|
|
44
56
|
};
|
|
45
57
|
submitButton: {
|
|
46
58
|
width: "100%";
|
|
59
|
+
height: number;
|
|
60
|
+
borderRadius: number;
|
|
47
61
|
};
|
|
48
62
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.40",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Request Entity
|
|
3
|
+
* Represents a community feature request that all users can see and vote on
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type FeatureRequestStatus = 'pending' | 'planned' | 'review' | 'completed' | 'dismissed';
|
|
7
|
+
|
|
8
|
+
export type VoteType = 'up' | 'down';
|
|
9
|
+
|
|
10
|
+
export interface FeatureRequestItem {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
type: string;
|
|
15
|
+
status: FeatureRequestStatus;
|
|
16
|
+
votes: number;
|
|
17
|
+
commentCount: number;
|
|
18
|
+
createdBy: string;
|
|
19
|
+
isAnonymous: boolean;
|
|
20
|
+
platform: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FeatureRequestVote {
|
|
26
|
+
type: VoteType;
|
|
27
|
+
votedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FeatureRequestSubmitData {
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
type: string;
|
|
34
|
+
rating?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FeatureRequestDataProvider {
|
|
38
|
+
/** Fetch all feature requests */
|
|
39
|
+
fetchRequests: () => Promise<FeatureRequestItem[]>;
|
|
40
|
+
/** Fetch current user's votes */
|
|
41
|
+
fetchUserVotes: (userId: string) => Promise<Record<string, VoteType>>;
|
|
42
|
+
/** Vote on a feature request */
|
|
43
|
+
onVote: (requestId: string, userId: string, type: VoteType) => Promise<void>;
|
|
44
|
+
/** Submit a new feature request */
|
|
45
|
+
onSubmit: (data: FeatureRequestSubmitData) => Promise<void>;
|
|
46
|
+
/** Current user ID (can be anonymous UID) */
|
|
47
|
+
userId: string | null;
|
|
48
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feedback Domain
|
|
3
|
-
* User feedback, bug reports
|
|
3
|
+
* User feedback, bug reports, feature requests
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from './presentation/components/FeedbackForm';
|
|
@@ -8,4 +8,5 @@ export * from './presentation/components/FeedbackModal';
|
|
|
8
8
|
export * from './presentation/components/SupportSection';
|
|
9
9
|
export * from './presentation/hooks/useFeedbackForm';
|
|
10
10
|
export * from './domain/entities/FeedbackEntity';
|
|
11
|
+
export * from './domain/entities/FeatureRequestEntity';
|
|
11
12
|
export * from './domain/repositories/IFeedbackRepository';
|
|
@@ -15,37 +15,51 @@ export const getFeedbackFormStyles = (_tokens: ReturnType<typeof useAppDesignTok
|
|
|
15
15
|
typeButton: {
|
|
16
16
|
flexDirection: "row",
|
|
17
17
|
alignItems: "center",
|
|
18
|
-
paddingHorizontal:
|
|
19
|
-
paddingVertical:
|
|
20
|
-
borderRadius:
|
|
21
|
-
borderWidth: 1,
|
|
22
|
-
|
|
18
|
+
paddingHorizontal: 20,
|
|
19
|
+
paddingVertical: 10,
|
|
20
|
+
borderRadius: 24,
|
|
21
|
+
borderWidth: 1.5,
|
|
22
|
+
marginRight: 10,
|
|
23
23
|
},
|
|
24
24
|
ratingContainer: {
|
|
25
25
|
alignItems: "center",
|
|
26
|
-
|
|
26
|
+
marginVertical: 32,
|
|
27
|
+
paddingVertical: 16,
|
|
28
|
+
backgroundColor: "rgba(255,255,255,0.02)",
|
|
29
|
+
borderRadius: 16,
|
|
30
|
+
},
|
|
31
|
+
ratingLabel: {
|
|
32
|
+
fontSize: 12,
|
|
33
|
+
fontWeight: "900",
|
|
34
|
+
letterSpacing: 1,
|
|
35
|
+
marginBottom: 16,
|
|
36
|
+
textTransform: "uppercase",
|
|
27
37
|
},
|
|
28
38
|
stars: {
|
|
29
39
|
flexDirection: "row",
|
|
30
|
-
gap:
|
|
40
|
+
gap: 12,
|
|
31
41
|
},
|
|
32
42
|
starButton: {
|
|
33
43
|
padding: 4,
|
|
34
44
|
},
|
|
35
45
|
inputContainer: {
|
|
36
|
-
marginBottom:
|
|
46
|
+
marginBottom: 32,
|
|
37
47
|
},
|
|
38
48
|
textArea: {
|
|
39
49
|
textAlignVertical: "top",
|
|
40
|
-
minHeight:
|
|
41
|
-
borderWidth: 1,
|
|
42
|
-
borderRadius:
|
|
43
|
-
padding:
|
|
50
|
+
minHeight: 140,
|
|
51
|
+
borderWidth: 1.5,
|
|
52
|
+
borderRadius: 16,
|
|
53
|
+
padding: 16,
|
|
54
|
+
fontSize: 15,
|
|
44
55
|
},
|
|
45
56
|
errorText: {
|
|
46
57
|
marginTop: 8,
|
|
58
|
+
fontWeight: "600",
|
|
47
59
|
},
|
|
48
60
|
submitButton: {
|
|
49
61
|
width: "100%",
|
|
62
|
+
height: 56,
|
|
63
|
+
borderRadius: 16,
|
|
50
64
|
},
|
|
51
65
|
});
|
|
@@ -67,7 +67,7 @@ const FeedbackRatingSection: React.FC<FeedbackRatingSectionProps> = ({
|
|
|
67
67
|
tokens,
|
|
68
68
|
}) => (
|
|
69
69
|
<View style={styles.ratingContainer}>
|
|
70
|
-
<AtomicText
|
|
70
|
+
<AtomicText style={[styles.ratingLabel, { color: tokens.colors.textSecondary }]}>
|
|
71
71
|
{ratingLabel}
|
|
72
72
|
</AtomicText>
|
|
73
73
|
<View style={styles.stars}>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
StyleSheet,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
ScrollView,
|
|
7
7
|
FlatList,
|
|
8
|
-
|
|
8
|
+
ActivityIndicator,
|
|
9
9
|
} from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
AtomicText,
|
|
@@ -15,177 +15,230 @@ import {
|
|
|
15
15
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
16
16
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
17
17
|
import { FeedbackModal } from "../components/FeedbackModal";
|
|
18
|
-
import type { FeedbackType
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
votes: number;
|
|
25
|
-
status: 'planned' | 'review' | 'completed';
|
|
26
|
-
comments: number;
|
|
27
|
-
userAvatars: string[];
|
|
28
|
-
voted: 'up' | 'down' | null;
|
|
29
|
-
}
|
|
18
|
+
import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
|
|
19
|
+
import type {
|
|
20
|
+
FeatureRequestItem,
|
|
21
|
+
FeatureRequestDataProvider,
|
|
22
|
+
VoteType,
|
|
23
|
+
} from "../../domain/entities/FeatureRequestEntity";
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
userAvatars: ["https://i.pravatar.cc/100?u=1", "https://i.pravatar.cc/100?u=2"],
|
|
40
|
-
voted: null,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "2",
|
|
44
|
-
title: "Couple Video Templates",
|
|
45
|
-
description: "Specific cinematic transition templates designed for couples' vlogs and anniversary montages.",
|
|
46
|
-
votes: 842,
|
|
47
|
-
status: 'review',
|
|
48
|
-
comments: 8,
|
|
49
|
-
userAvatars: ["https://i.pravatar.cc/100?u=3"],
|
|
50
|
-
voted: null,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "3",
|
|
54
|
-
title: "4K Export Support",
|
|
55
|
-
description: "Allow users to export generated AI videos in high-quality 4K resolution.",
|
|
56
|
-
votes: 2100,
|
|
57
|
-
status: 'completed',
|
|
58
|
-
comments: 42,
|
|
59
|
-
userAvatars: ["https://i.pravatar.cc/100?u=4", "https://i.pravatar.cc/100?u=5"],
|
|
60
|
-
voted: null,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "4",
|
|
64
|
-
title: "Dark Mode Editor",
|
|
65
|
-
description: "Make the entire editing interface dark for better focus during late-night creative sessions.",
|
|
66
|
-
votes: 156,
|
|
67
|
-
status: 'planned',
|
|
68
|
-
comments: 5,
|
|
69
|
-
userAvatars: ["https://i.pravatar.cc/100?u=6"],
|
|
70
|
-
voted: null,
|
|
71
|
-
}
|
|
72
|
-
];
|
|
25
|
+
interface FeatureRequestScreenProps {
|
|
26
|
+
config?: {
|
|
27
|
+
translations?: Record<string, any>;
|
|
28
|
+
dataProvider?: FeatureRequestDataProvider;
|
|
29
|
+
onSubmit?: (data: any) => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
texts: any;
|
|
32
|
+
}
|
|
73
33
|
|
|
74
|
-
export const FeatureRequestScreen: React.FC<
|
|
34
|
+
export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
|
|
75
35
|
const tokens = useAppDesignTokens();
|
|
76
|
-
const
|
|
36
|
+
const dataProvider = config?.dataProvider;
|
|
37
|
+
|
|
38
|
+
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
39
|
+
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
40
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
77
41
|
const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
|
|
78
42
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
79
44
|
|
|
80
|
-
// Use passed translations if available
|
|
81
45
|
const t = config?.translations || {};
|
|
82
46
|
const screenTitle = t.screen_title || "Feedback & Features";
|
|
83
47
|
const trendingTitle = t.trending || "Trending Requests";
|
|
84
48
|
const bannerTitle = t.banner?.title || "Community Active";
|
|
85
|
-
const bannerSub = t.banner?.subtitle ||
|
|
49
|
+
const bannerSub = t.banner?.subtitle || `${requests.length} feature requests`;
|
|
86
50
|
const newIdeaLabel = t.new_idea || "NEW IDEA?";
|
|
87
|
-
|
|
51
|
+
|
|
88
52
|
const tabLabels = {
|
|
89
53
|
all: t.tabs?.all || "All Requests",
|
|
90
54
|
my: t.tabs?.my || "My Feedback",
|
|
91
55
|
roadmap: t.tabs?.roadmap || "Roadmap",
|
|
92
56
|
};
|
|
93
57
|
|
|
94
|
-
const statusLabels = {
|
|
58
|
+
const statusLabels: Record<string, string> = {
|
|
95
59
|
planned: t.status?.planned || "Planned",
|
|
96
60
|
review: t.status?.review || "Under Review",
|
|
97
61
|
completed: t.status?.completed || "Completed",
|
|
62
|
+
pending: t.status?.pending || "Pending",
|
|
63
|
+
dismissed: t.status?.dismissed || "Dismissed",
|
|
98
64
|
};
|
|
99
65
|
|
|
100
|
-
const
|
|
66
|
+
const loadData = useCallback(async () => {
|
|
67
|
+
if (!dataProvider) {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
setIsLoading(true);
|
|
74
|
+
const [fetchedRequests, fetchedVotes] = await Promise.all([
|
|
75
|
+
dataProvider.fetchRequests(),
|
|
76
|
+
dataProvider.userId
|
|
77
|
+
? dataProvider.fetchUserVotes(dataProvider.userId)
|
|
78
|
+
: Promise.resolve({} as Record<string, VoteType>),
|
|
79
|
+
]);
|
|
80
|
+
setRequests(fetchedRequests);
|
|
81
|
+
setUserVotes(fetchedVotes);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (__DEV__) console.warn("[FeatureRequestScreen] Failed to load data:", error);
|
|
84
|
+
} finally {
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
}
|
|
87
|
+
}, [dataProvider]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
loadData();
|
|
91
|
+
}, [loadData]);
|
|
92
|
+
|
|
93
|
+
const handleVote = useCallback(async (id: string, type: VoteType) => {
|
|
94
|
+
if (!dataProvider?.userId || !dataProvider.onVote) return;
|
|
95
|
+
|
|
96
|
+
const previousVote = userVotes[id];
|
|
97
|
+
const isUndoVote = previousVote === type;
|
|
98
|
+
|
|
99
|
+
// Optimistic update
|
|
100
|
+
setUserVotes(prev => {
|
|
101
|
+
const next = { ...prev };
|
|
102
|
+
if (isUndoVote) {
|
|
103
|
+
delete next[id];
|
|
104
|
+
} else {
|
|
105
|
+
next[id] = type;
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
|
|
101
110
|
setRequests(prev => prev.map(req => {
|
|
102
|
-
if (req.id
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
newVotes += type === 'up' ? 2 : -2;
|
|
111
|
-
} else {
|
|
112
|
-
newVotes += type === 'up' ? 1 : -1;
|
|
113
|
-
}
|
|
114
|
-
return { ...req, votes: newVotes, voted: type };
|
|
115
|
-
}
|
|
111
|
+
if (req.id !== id) return req;
|
|
112
|
+
let delta = 0;
|
|
113
|
+
if (isUndoVote) {
|
|
114
|
+
delta = type === 'up' ? -1 : 1;
|
|
115
|
+
} else if (previousVote) {
|
|
116
|
+
delta = type === 'up' ? 2 : -2;
|
|
117
|
+
} else {
|
|
118
|
+
delta = type === 'up' ? 1 : -1;
|
|
116
119
|
}
|
|
117
|
-
return req;
|
|
120
|
+
return { ...req, votes: req.votes + delta };
|
|
118
121
|
}));
|
|
119
|
-
};
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
try {
|
|
124
|
+
await dataProvider.onVote(id, dataProvider.userId, type);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Revert on failure
|
|
127
|
+
setUserVotes(prev => {
|
|
128
|
+
const next = { ...prev };
|
|
129
|
+
if (previousVote) {
|
|
130
|
+
next[id] = previousVote;
|
|
131
|
+
} else {
|
|
132
|
+
delete next[id];
|
|
133
|
+
}
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
loadData();
|
|
137
|
+
if (__DEV__) console.warn("[FeatureRequestScreen] Vote failed:", error);
|
|
138
|
+
}
|
|
139
|
+
}, [dataProvider, userVotes, loadData]);
|
|
140
|
+
|
|
141
|
+
const handleSubmit = useCallback(async (data: any) => {
|
|
142
|
+
if (!dataProvider?.onSubmit) return;
|
|
143
|
+
|
|
144
|
+
setIsSubmitting(true);
|
|
145
|
+
try {
|
|
146
|
+
await dataProvider.onSubmit({
|
|
147
|
+
title: data.title || "New Request",
|
|
148
|
+
description: data.description,
|
|
149
|
+
type: data.type || "feature_request",
|
|
150
|
+
rating: data.rating,
|
|
151
|
+
});
|
|
152
|
+
setIsModalVisible(false);
|
|
153
|
+
// Reload data to get the new request from the database
|
|
154
|
+
await loadData();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (__DEV__) console.warn("[FeatureRequestScreen] Submit failed:", error);
|
|
157
|
+
} finally {
|
|
158
|
+
setIsSubmitting(false);
|
|
159
|
+
}
|
|
160
|
+
}, [dataProvider, loadData]);
|
|
161
|
+
|
|
162
|
+
const getStatusColor = (status: string) => {
|
|
122
163
|
switch (status) {
|
|
123
164
|
case 'planned': return '#3b82f6';
|
|
124
165
|
case 'review': return '#f59e0b';
|
|
125
166
|
case 'completed': return '#10b981';
|
|
167
|
+
case 'pending': return '#8b5cf6';
|
|
168
|
+
case 'dismissed': return '#ef4444';
|
|
126
169
|
default: return tokens.colors.textSecondary;
|
|
127
170
|
}
|
|
128
171
|
};
|
|
129
172
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<AtomicText style={[styles.voteCount, { color: item.voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
141
|
-
{item.votes}
|
|
142
|
-
</AtomicText>
|
|
143
|
-
<TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
|
|
144
|
-
<AtomicIcon
|
|
145
|
-
name="expand-more"
|
|
146
|
-
size="md"
|
|
147
|
-
color={item.voted === 'down' ? "primary" : "textSecondary" as any}
|
|
148
|
-
/>
|
|
149
|
-
</TouchableOpacity>
|
|
150
|
-
</View>
|
|
173
|
+
const filteredRequests = (() => {
|
|
174
|
+
switch (activeTab) {
|
|
175
|
+
case 'my':
|
|
176
|
+
return requests.filter(r => r.createdBy === dataProvider?.userId);
|
|
177
|
+
case 'roadmap':
|
|
178
|
+
return requests.filter(r => r.status === 'planned' || r.status === 'completed' || r.status === 'review');
|
|
179
|
+
default:
|
|
180
|
+
return requests;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
151
183
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
184
|
+
const renderRequestItem = ({ item }: { item: FeatureRequestItem }) => {
|
|
185
|
+
const voted = userVotes[item.id] || null;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
|
|
189
|
+
<View style={styles.voteColumn}>
|
|
190
|
+
<TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
|
|
191
|
+
<AtomicIcon
|
|
192
|
+
name="chevron-up"
|
|
193
|
+
size="md"
|
|
194
|
+
color={voted === 'up' ? "primary" : "textSecondary" as any}
|
|
195
|
+
/>
|
|
196
|
+
</TouchableOpacity>
|
|
197
|
+
<AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
198
|
+
{item.votes}
|
|
199
|
+
</AtomicText>
|
|
200
|
+
<TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
|
|
201
|
+
<AtomicIcon
|
|
202
|
+
name="chevron-down"
|
|
203
|
+
size="md"
|
|
204
|
+
color={voted === 'down' ? "primary" : "textSecondary" as any}
|
|
205
|
+
/>
|
|
206
|
+
</TouchableOpacity>
|
|
160
207
|
</View>
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
))}
|
|
171
|
-
{item.comments > 5 && (
|
|
172
|
-
<View style={[styles.avatarMore, { backgroundColor: tokens.colors.surfaceVariant }]}>
|
|
173
|
-
<AtomicText style={styles.avatarMoreText}>+{item.comments}</AtomicText>
|
|
174
|
-
</View>
|
|
175
|
-
)}
|
|
208
|
+
|
|
209
|
+
<View style={styles.cardContent}>
|
|
210
|
+
<View style={styles.cardHeader}>
|
|
211
|
+
<AtomicText style={styles.cardTitle}>{item.title}</AtomicText>
|
|
212
|
+
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) + '20', borderColor: getStatusColor(item.status) + '40' }]}>
|
|
213
|
+
<AtomicText style={[styles.statusText, { color: getStatusColor(item.status) }]}>
|
|
214
|
+
{(statusLabels[item.status] || item.status).toUpperCase()}
|
|
215
|
+
</AtomicText>
|
|
216
|
+
</View>
|
|
176
217
|
</View>
|
|
177
|
-
|
|
178
|
-
|
|
218
|
+
|
|
219
|
+
<AtomicText style={[styles.cardDescription, { color: tokens.colors.textSecondary }]}>
|
|
220
|
+
{item.description}
|
|
179
221
|
</AtomicText>
|
|
222
|
+
|
|
223
|
+
<View style={styles.cardFooter}>
|
|
224
|
+
<View style={styles.platformBadge}>
|
|
225
|
+
<AtomicText style={[styles.platformText, { color: tokens.colors.textTertiary }]}>
|
|
226
|
+
{item.platform}
|
|
227
|
+
</AtomicText>
|
|
228
|
+
</View>
|
|
229
|
+
<AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
|
|
230
|
+
{item.commentCount} {t.comment_count?.replace('{{count}}', '') || 'comments'}
|
|
231
|
+
</AtomicText>
|
|
232
|
+
</View>
|
|
180
233
|
</View>
|
|
181
234
|
</View>
|
|
182
|
-
|
|
183
|
-
|
|
235
|
+
);
|
|
236
|
+
};
|
|
184
237
|
|
|
185
238
|
const header = (
|
|
186
239
|
<View style={styles.header}>
|
|
187
240
|
<AtomicText style={styles.headerTitle}>{screenTitle}</AtomicText>
|
|
188
|
-
<TouchableOpacity
|
|
241
|
+
<TouchableOpacity
|
|
189
242
|
style={[styles.addButton, { backgroundColor: tokens.colors.primary }]}
|
|
190
243
|
onPress={() => setIsModalVisible(true)}
|
|
191
244
|
>
|
|
@@ -194,11 +247,21 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
|
194
247
|
</View>
|
|
195
248
|
);
|
|
196
249
|
|
|
250
|
+
if (isLoading) {
|
|
251
|
+
return (
|
|
252
|
+
<ScreenLayout header={header} edges={['top', 'bottom']}>
|
|
253
|
+
<View style={styles.loadingContainer}>
|
|
254
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
255
|
+
</View>
|
|
256
|
+
</ScreenLayout>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
197
260
|
return (
|
|
198
261
|
<ScreenLayout header={header} edges={['top', 'bottom']}>
|
|
199
262
|
<View style={styles.tabsContainer}>
|
|
200
263
|
{(['all', 'my', 'roadmap'] as const).map((tab) => (
|
|
201
|
-
<TouchableOpacity
|
|
264
|
+
<TouchableOpacity
|
|
202
265
|
key={tab}
|
|
203
266
|
onPress={() => setActiveTab(tab)}
|
|
204
267
|
style={[styles.tab, activeTab === tab && { borderBottomColor: tokens.colors.primary }]}
|
|
@@ -226,44 +289,43 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
|
226
289
|
|
|
227
290
|
<AtomicText style={styles.sectionTitle}>{trendingTitle}</AtomicText>
|
|
228
291
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
292
|
+
{filteredRequests.length === 0 ? (
|
|
293
|
+
<View style={styles.emptyState}>
|
|
294
|
+
<AtomicIcon name="chatbubble-outline" size="xl" color="textTertiary" />
|
|
295
|
+
<AtomicText style={[styles.emptyText, { color: tokens.colors.textTertiary }]}>
|
|
296
|
+
{t.empty || "No requests yet. Be the first!"}
|
|
297
|
+
</AtomicText>
|
|
298
|
+
</View>
|
|
299
|
+
) : (
|
|
300
|
+
<FlatList
|
|
301
|
+
data={filteredRequests}
|
|
302
|
+
renderItem={renderRequestItem}
|
|
303
|
+
keyExtractor={item => item.id}
|
|
304
|
+
scrollEnabled={false}
|
|
305
|
+
contentContainerStyle={styles.listContent}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
236
308
|
</ScrollView>
|
|
237
309
|
|
|
238
310
|
{isModalVisible && (
|
|
239
311
|
<FeedbackModal
|
|
240
312
|
visible={isModalVisible}
|
|
241
313
|
onClose={() => setIsModalVisible(false)}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
setIsModalVisible(false);
|
|
245
|
-
// Add to mock list for "live" feel
|
|
246
|
-
const newReq: FeatureRequest = {
|
|
247
|
-
id: Date.now().toString(),
|
|
248
|
-
title: data.title || "New Request",
|
|
249
|
-
description: data.description,
|
|
250
|
-
votes: 1,
|
|
251
|
-
status: 'review',
|
|
252
|
-
comments: 0,
|
|
253
|
-
userAvatars: ["https://i.pravatar.cc/100?u=me"],
|
|
254
|
-
voted: 'up',
|
|
255
|
-
};
|
|
256
|
-
setRequests(prev => [newReq, ...prev]);
|
|
257
|
-
}}
|
|
314
|
+
title={texts?.title}
|
|
315
|
+
onSubmit={handleSubmit}
|
|
258
316
|
texts={texts}
|
|
259
317
|
initialType="feature_request"
|
|
318
|
+
isSubmitting={isSubmitting}
|
|
260
319
|
/>
|
|
261
320
|
)}
|
|
262
321
|
|
|
263
322
|
<View style={styles.floatingHint}>
|
|
264
|
-
<
|
|
323
|
+
<TouchableOpacity
|
|
324
|
+
style={[styles.hintBadge, { backgroundColor: tokens.colors.primary }]}
|
|
325
|
+
onPress={() => setIsModalVisible(true)}
|
|
326
|
+
>
|
|
265
327
|
<AtomicText style={styles.hintText}>{newIdeaLabel}</AtomicText>
|
|
266
|
-
</
|
|
328
|
+
</TouchableOpacity>
|
|
267
329
|
</View>
|
|
268
330
|
</ScreenLayout>
|
|
269
331
|
);
|
|
@@ -396,30 +458,14 @@ const styles = StyleSheet.create({
|
|
|
396
458
|
justifyContent: 'space-between',
|
|
397
459
|
marginTop: 4,
|
|
398
460
|
},
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
avatar: {
|
|
404
|
-
width: 24,
|
|
405
|
-
height: 24,
|
|
406
|
-
borderRadius: 12,
|
|
407
|
-
borderWidth: 2,
|
|
408
|
-
marginLeft: -8,
|
|
409
|
-
},
|
|
410
|
-
avatarMore: {
|
|
411
|
-
width: 24,
|
|
412
|
-
height: 24,
|
|
413
|
-
borderRadius: 12,
|
|
414
|
-
alignItems: 'center',
|
|
415
|
-
justifyContent: 'center',
|
|
416
|
-
marginLeft: -8,
|
|
417
|
-
borderWidth: 2,
|
|
418
|
-
borderColor: 'transparent',
|
|
461
|
+
platformBadge: {
|
|
462
|
+
paddingHorizontal: 6,
|
|
463
|
+
paddingVertical: 2,
|
|
419
464
|
},
|
|
420
|
-
|
|
421
|
-
fontSize:
|
|
422
|
-
fontWeight: '
|
|
465
|
+
platformText: {
|
|
466
|
+
fontSize: 10,
|
|
467
|
+
fontWeight: '600',
|
|
468
|
+
textTransform: 'uppercase',
|
|
423
469
|
},
|
|
424
470
|
commentCount: {
|
|
425
471
|
fontSize: 11,
|
|
@@ -445,5 +491,21 @@ const styles = StyleSheet.create({
|
|
|
445
491
|
fontSize: 10,
|
|
446
492
|
fontWeight: '900',
|
|
447
493
|
textTransform: 'uppercase',
|
|
448
|
-
}
|
|
494
|
+
},
|
|
495
|
+
loadingContainer: {
|
|
496
|
+
flex: 1,
|
|
497
|
+
justifyContent: 'center',
|
|
498
|
+
alignItems: 'center',
|
|
499
|
+
paddingTop: 100,
|
|
500
|
+
},
|
|
501
|
+
emptyState: {
|
|
502
|
+
alignItems: 'center',
|
|
503
|
+
justifyContent: 'center',
|
|
504
|
+
paddingVertical: 60,
|
|
505
|
+
gap: 12,
|
|
506
|
+
},
|
|
507
|
+
emptyText: {
|
|
508
|
+
fontSize: 14,
|
|
509
|
+
fontWeight: '500',
|
|
510
|
+
},
|
|
449
511
|
});
|
|
@@ -14,6 +14,7 @@ import { translateFAQData } from "../utils/faqTranslator";
|
|
|
14
14
|
import { useSettingsConfigFactory } from "../utils/settingsConfigFactory";
|
|
15
15
|
import type { SettingsConfig, SettingsTranslations } from "../screens/types";
|
|
16
16
|
import type { FeedbackFormData } from "../utils/config-creators";
|
|
17
|
+
import type { FeatureRequestDataProvider } from "../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
17
18
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
18
19
|
|
|
19
20
|
export interface SettingsFeatures {
|
|
@@ -35,6 +36,7 @@ export interface UseSettingsScreenConfigParams {
|
|
|
35
36
|
faqData?: FAQData;
|
|
36
37
|
isPremium: boolean;
|
|
37
38
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
39
|
+
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
38
40
|
additionalScreens?: AdditionalScreen[];
|
|
39
41
|
features?: SettingsFeatures;
|
|
40
42
|
translations?: SettingsTranslations;
|
|
@@ -52,7 +54,7 @@ export interface SettingsScreenConfigResult {
|
|
|
52
54
|
export const useSettingsScreenConfig = (
|
|
53
55
|
params: UseSettingsScreenConfigParams
|
|
54
56
|
): SettingsScreenConfigResult => {
|
|
55
|
-
const { appInfo, faqData, isPremium, onFeedbackSubmit, features = {}, translations } = params;
|
|
57
|
+
const { appInfo, faqData, isPremium, onFeedbackSubmit, featureRequestDataProvider, features = {}, translations } = params;
|
|
56
58
|
|
|
57
59
|
const {
|
|
58
60
|
notifications: showNotifications = true,
|
|
@@ -102,6 +104,15 @@ export const useSettingsScreenConfig = (
|
|
|
102
104
|
translations,
|
|
103
105
|
};
|
|
104
106
|
|
|
107
|
+
// Merge dataProvider into feedback config
|
|
108
|
+
if (featureRequestDataProvider && config.feedback) {
|
|
109
|
+
const existingFeedback = typeof config.feedback === 'object' ? config.feedback : { enabled: true };
|
|
110
|
+
config.feedback = {
|
|
111
|
+
...existingFeedback,
|
|
112
|
+
dataProvider: featureRequestDataProvider,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
if (config.subscription && typeof config.subscription === 'object') {
|
|
106
117
|
config.subscription = {
|
|
107
118
|
...config.subscription,
|
|
@@ -132,7 +143,7 @@ export const useSettingsScreenConfig = (
|
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
return config;
|
|
135
|
-
}, [baseSettingsConfig, translations]);
|
|
146
|
+
}, [baseSettingsConfig, translations, featureRequestDataProvider]);
|
|
136
147
|
|
|
137
148
|
const userProfile = useMemo(() => createUserProfileDisplay({
|
|
138
149
|
profileData: userProfileData,
|
|
@@ -133,8 +133,13 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
133
133
|
title: videoTutorialConfig?.title || featureTranslations?.videoTutorial?.title || "",
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
const feedbackConfig = typeof config?.feedback === 'object' ? config.feedback : undefined;
|
|
136
137
|
const featureRequestScreen = createScreenWithProps("FeatureRequest", FeatureRequestScreen, {
|
|
137
|
-
config:
|
|
138
|
+
config: {
|
|
139
|
+
...feedbackConfig,
|
|
140
|
+
translations: translations?.feedback,
|
|
141
|
+
dataProvider: feedbackConfig?.dataProvider,
|
|
142
|
+
},
|
|
138
143
|
texts: {
|
|
139
144
|
feedbackTypes: [
|
|
140
145
|
{ type: 'general', label: translations?.feedbackModal?.types?.general || 'General' },
|
|
@@ -147,6 +152,17 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
147
152
|
descriptionPlaceholder: translations?.feedbackModal?.descriptionPlaceholder || 'Tell us more...',
|
|
148
153
|
submitButton: translations?.feedbackModal?.submitButton || 'Submit',
|
|
149
154
|
submittingButton: translations?.feedbackModal?.submittingButton || 'Submitting...',
|
|
155
|
+
title: translations?.feedbackModal?.title || 'Submit Feedback',
|
|
156
|
+
defaultTitle: (type: string) => {
|
|
157
|
+
const titles: Record<string, string> = {
|
|
158
|
+
general: translations?.feedbackModal?.types?.general || type,
|
|
159
|
+
bug_report: translations?.feedbackModal?.types?.bugReport || type,
|
|
160
|
+
feature_request: translations?.feedbackModal?.types?.featureRequest || type,
|
|
161
|
+
improvement: translations?.feedbackModal?.types?.improvement || type,
|
|
162
|
+
other: translations?.feedbackModal?.types?.other || type,
|
|
163
|
+
};
|
|
164
|
+
return titles[type] || type;
|
|
165
|
+
},
|
|
150
166
|
}
|
|
151
167
|
});
|
|
152
168
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { FeatureVisibility } from "./BaseTypes";
|
|
7
7
|
import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
|
|
8
|
+
import type { FeatureRequestDataProvider } from "../../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
8
9
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
9
10
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
10
11
|
|
|
@@ -44,6 +45,8 @@ export interface FeedbackConfig {
|
|
|
44
45
|
onSubmit?: (data: { type: FeedbackType; rating: number; description: string; title: string }) => Promise<void>;
|
|
45
46
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
46
47
|
onPress?: () => void;
|
|
48
|
+
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
49
|
+
dataProvider?: FeatureRequestDataProvider;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|