@upstep/react-native 0.5.0 → 0.6.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 +2 -5
- package/src/FeedbackContext.tsx +15 -9
- package/src/FeedbackSheet.tsx +356 -320
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstep/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Upstep feedback SDK for React Native",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -15,11 +15,8 @@
|
|
|
15
15
|
"@upstep/types": "0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@gorhom/bottom-sheet": ">=4",
|
|
19
18
|
"react": ">=18",
|
|
20
|
-
"react-native": ">=0.73"
|
|
21
|
-
"react-native-gesture-handler": ">=2",
|
|
22
|
-
"react-native-reanimated": ">=3"
|
|
19
|
+
"react-native": ">=0.73"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"@types/react": "^19.0.7",
|
package/src/FeedbackContext.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import React, {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useState,
|
|
6
6
|
useCallback,
|
|
7
|
-
useRef,
|
|
8
7
|
type ReactNode,
|
|
9
8
|
} from "react";
|
|
10
9
|
import type {
|
|
@@ -15,22 +14,21 @@ import type {
|
|
|
15
14
|
VoteValue,
|
|
16
15
|
} from "@upstep/types";
|
|
17
16
|
import { UpstepApiClient } from "./api";
|
|
18
|
-
import type BottomSheet from "@gorhom/bottom-sheet";
|
|
19
17
|
|
|
20
18
|
const DEFAULT_ACCENT = "#D97757";
|
|
21
19
|
|
|
22
20
|
interface UpstepContextValue {
|
|
23
21
|
client: UpstepApiClient;
|
|
24
22
|
feedItems: Feedback[];
|
|
23
|
+
feedLoading: boolean;
|
|
25
24
|
loadFeed: () => Promise<void>;
|
|
26
25
|
submit: (title: string, content: string, type?: FeedbackType) => Promise<void>;
|
|
27
26
|
vote: (feedbackId: string, value: VoteValue) => Promise<void>;
|
|
28
27
|
getItem: (feedbackId: string) => Promise<FeedbackWithComments>;
|
|
29
|
-
|
|
28
|
+
isOpen: boolean;
|
|
30
29
|
openSheet: () => void;
|
|
31
|
-
|
|
30
|
+
closeSheet: () => void;
|
|
32
31
|
identify: (userId: string | undefined) => void;
|
|
33
|
-
sheetRef: React.RefObject<BottomSheet | null>;
|
|
34
32
|
accentColor: string;
|
|
35
33
|
theme: "light" | "dark" | "auto";
|
|
36
34
|
}
|
|
@@ -43,18 +41,22 @@ export function FeedbackProvider({
|
|
|
43
41
|
}: UpstepConfig & { children: ReactNode }) {
|
|
44
42
|
const [client] = useState(() => new UpstepApiClient(config));
|
|
45
43
|
const [feedItems, setFeedItems] = useState<Feedback[]>([]);
|
|
46
|
-
const
|
|
44
|
+
const [feedLoading, setFeedLoading] = useState(true);
|
|
45
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
47
46
|
|
|
48
47
|
useEffect(() => {
|
|
49
48
|
client.setUserId(config.userId);
|
|
50
49
|
}, [client, config.userId]);
|
|
51
50
|
|
|
52
51
|
const loadFeed = useCallback(async () => {
|
|
52
|
+
setFeedLoading(true);
|
|
53
53
|
try {
|
|
54
54
|
const data = await client.listFeedback({ sort: "votes", limit: 30 });
|
|
55
55
|
setFeedItems(data.items);
|
|
56
56
|
} catch {
|
|
57
57
|
// non-critical
|
|
58
|
+
} finally {
|
|
59
|
+
setFeedLoading(false);
|
|
58
60
|
}
|
|
59
61
|
}, [client]);
|
|
60
62
|
|
|
@@ -82,7 +84,11 @@ export function FeedbackProvider({
|
|
|
82
84
|
);
|
|
83
85
|
|
|
84
86
|
const openSheet = useCallback(() => {
|
|
85
|
-
|
|
87
|
+
setIsOpen(true);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const closeSheet = useCallback(() => {
|
|
91
|
+
setIsOpen(false);
|
|
86
92
|
}, []);
|
|
87
93
|
|
|
88
94
|
const identify = useCallback(
|
|
@@ -93,8 +99,8 @@ export function FeedbackProvider({
|
|
|
93
99
|
return (
|
|
94
100
|
<UpstepContext.Provider
|
|
95
101
|
value={{
|
|
96
|
-
client, feedItems, loadFeed, submit, vote, getItem,
|
|
97
|
-
openSheet,
|
|
102
|
+
client, feedItems, feedLoading, loadFeed, submit, vote, getItem,
|
|
103
|
+
isOpen, openSheet, closeSheet, identify,
|
|
98
104
|
accentColor: config.accentColor ?? DEFAULT_ACCENT,
|
|
99
105
|
theme: config.theme ?? "auto",
|
|
100
106
|
}}
|
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Modal,
|
|
3
4
|
View,
|
|
4
5
|
Text,
|
|
5
6
|
TextInput,
|
|
6
7
|
TouchableOpacity,
|
|
8
|
+
Pressable,
|
|
9
|
+
ScrollView,
|
|
10
|
+
KeyboardAvoidingView,
|
|
7
11
|
StyleSheet,
|
|
8
12
|
ActivityIndicator,
|
|
9
|
-
ScrollView,
|
|
10
13
|
useColorScheme,
|
|
11
14
|
Platform,
|
|
15
|
+
SafeAreaView,
|
|
12
16
|
} from "react-native";
|
|
13
|
-
import BottomSheet, { BottomSheetView, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
|
14
17
|
import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
|
|
15
18
|
import { useUpstep } from "./FeedbackContext";
|
|
16
19
|
|
|
@@ -19,37 +22,31 @@ import { useUpstep } from "./FeedbackContext";
|
|
|
19
22
|
interface Palette {
|
|
20
23
|
bg: string;
|
|
21
24
|
bgSoft: string;
|
|
22
|
-
bgHover: string;
|
|
23
25
|
text: string;
|
|
24
26
|
textSoft: string;
|
|
25
27
|
textFaint: string;
|
|
26
28
|
border: string;
|
|
27
29
|
handle: string;
|
|
28
|
-
shadow: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const LIGHT: Palette = {
|
|
32
33
|
bg: "#ffffff",
|
|
33
34
|
bgSoft: "#f6f5f2",
|
|
34
|
-
bgHover: "#efede8",
|
|
35
35
|
text: "#1a1915",
|
|
36
36
|
textSoft: "#56544d",
|
|
37
37
|
textFaint: "#9b9890",
|
|
38
38
|
border: "#e8e6df",
|
|
39
39
|
handle: "#dad7ce",
|
|
40
|
-
shadow: "#00000014",
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
const DARK: Palette = {
|
|
44
43
|
bg: "#1c1b19",
|
|
45
44
|
bgSoft: "#262522",
|
|
46
|
-
bgHover: "#302e2a",
|
|
47
45
|
text: "#f5f4f0",
|
|
48
46
|
textSoft: "#b4b1a8",
|
|
49
47
|
textFaint: "#7d7a72",
|
|
50
48
|
border: "#33312c",
|
|
51
49
|
handle: "#48453f",
|
|
52
|
-
shadow: "#0000003a",
|
|
53
50
|
};
|
|
54
51
|
|
|
55
52
|
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
@@ -62,81 +59,159 @@ type Nav =
|
|
|
62
59
|
// ─── FeedbackSheet ────────────────────────────────────────────────────────────
|
|
63
60
|
|
|
64
61
|
export function FeedbackSheet() {
|
|
65
|
-
const {
|
|
62
|
+
const { isOpen, closeSheet, accentColor, theme, loadFeed } = useUpstep();
|
|
66
63
|
const scheme = useColorScheme();
|
|
67
64
|
const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
|
|
68
65
|
const p = isDark ? DARK : LIGHT;
|
|
69
66
|
|
|
70
67
|
const [nav, setNav] = useState<Nav>({ screen: "list" });
|
|
71
|
-
const snapPoints = useMemo(() => ["90%"], []);
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
// Refresh list every time the modal opens
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isOpen) loadFeed();
|
|
72
|
+
}, [isOpen, loadFeed]);
|
|
73
|
+
|
|
74
|
+
function handleClose() {
|
|
75
|
+
closeSheet();
|
|
76
|
+
// Reset to list when closed so next open starts fresh
|
|
77
|
+
setTimeout(() => setNav({ screen: "list" }), 300);
|
|
78
|
+
}
|
|
76
79
|
|
|
77
80
|
return (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
|
|
85
|
-
onClose={() => setNav({ screen: "list" })}
|
|
81
|
+
<Modal
|
|
82
|
+
visible={isOpen}
|
|
83
|
+
animationType="slide"
|
|
84
|
+
transparent
|
|
85
|
+
statusBarTranslucent
|
|
86
|
+
onRequestClose={handleClose}
|
|
86
87
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
<KeyboardAvoidingView
|
|
89
|
+
style={styles.overlay}
|
|
90
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
91
|
+
>
|
|
92
|
+
{/* Tap backdrop to dismiss */}
|
|
93
|
+
<Pressable style={styles.backdrop} onPress={handleClose} />
|
|
94
|
+
|
|
95
|
+
{/* Sheet */}
|
|
96
|
+
<View style={[styles.sheet, { backgroundColor: p.bg }]}>
|
|
97
|
+
{/* Drag handle */}
|
|
98
|
+
<View style={styles.handleWrap}>
|
|
99
|
+
<View style={[styles.handle, { backgroundColor: p.handle }]} />
|
|
100
|
+
</View>
|
|
101
|
+
|
|
102
|
+
{/* Fixed header */}
|
|
103
|
+
<View style={[styles.header, { borderBottomColor: p.border }]}>
|
|
104
|
+
{nav.screen === "list" ? (
|
|
105
|
+
<>
|
|
106
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
|
|
107
|
+
<TouchableOpacity
|
|
108
|
+
onPress={() => setNav({ screen: "create" })}
|
|
109
|
+
activeOpacity={0.8}
|
|
110
|
+
style={[styles.newBtn, { backgroundColor: accentColor }]}
|
|
111
|
+
>
|
|
112
|
+
<Text style={styles.newBtnText}>+ New</Text>
|
|
113
|
+
</TouchableOpacity>
|
|
114
|
+
</>
|
|
115
|
+
) : nav.screen === "create" ? (
|
|
116
|
+
<>
|
|
117
|
+
<TouchableOpacity
|
|
118
|
+
onPress={() => setNav({ screen: "list" })}
|
|
119
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
120
|
+
>
|
|
121
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
|
|
122
|
+
</TouchableOpacity>
|
|
123
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
|
|
124
|
+
<View style={styles.navBtnSpacer} />
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
<TouchableOpacity
|
|
128
|
+
onPress={() => setNav({ screen: "list" })}
|
|
129
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
130
|
+
>
|
|
131
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
|
|
132
|
+
</TouchableOpacity>
|
|
133
|
+
)}
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
{/* Scrollable body */}
|
|
137
|
+
<ScrollView
|
|
138
|
+
style={styles.scroll}
|
|
139
|
+
contentContainerStyle={styles.scrollContent}
|
|
140
|
+
keyboardShouldPersistTaps="handled"
|
|
141
|
+
showsVerticalScrollIndicator={false}
|
|
142
|
+
>
|
|
143
|
+
{nav.screen === "list" && (
|
|
144
|
+
<FeedListBody
|
|
145
|
+
p={p}
|
|
146
|
+
accent={accentColor}
|
|
147
|
+
onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
{nav.screen === "detail" && "feedbackId" in nav && (
|
|
151
|
+
<FeedDetailBody
|
|
152
|
+
p={p}
|
|
153
|
+
accent={accentColor}
|
|
154
|
+
feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
{nav.screen === "create" && (
|
|
158
|
+
<FeedCreateBody
|
|
159
|
+
p={p}
|
|
160
|
+
accent={accentColor}
|
|
161
|
+
onDone={() => {
|
|
162
|
+
setNav({ screen: "list" });
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</ScrollView>
|
|
167
|
+
|
|
168
|
+
{/* Safe area spacer */}
|
|
169
|
+
<SafeAreaView />
|
|
170
|
+
</View>
|
|
171
|
+
</KeyboardAvoidingView>
|
|
172
|
+
</Modal>
|
|
97
173
|
);
|
|
98
174
|
}
|
|
99
175
|
|
|
100
|
-
// ───
|
|
176
|
+
// ─── FeedListBody ─────────────────────────────────────────────────────────────
|
|
101
177
|
|
|
102
|
-
function
|
|
103
|
-
p, accent, onSelect,
|
|
178
|
+
function FeedListBody({
|
|
179
|
+
p, accent, onSelect,
|
|
104
180
|
}: {
|
|
105
|
-
p: Palette; accent: string;
|
|
106
|
-
onSelect: (id: string) => void;
|
|
107
|
-
onCreate: () => void;
|
|
181
|
+
p: Palette; accent: string; onSelect: (id: string) => void;
|
|
108
182
|
}) {
|
|
109
|
-
const { feedItems, vote } = useUpstep();
|
|
183
|
+
const { feedItems, feedLoading, vote } = useUpstep();
|
|
184
|
+
|
|
185
|
+
if (feedLoading) {
|
|
186
|
+
return (
|
|
187
|
+
<View style={styles.centered}>
|
|
188
|
+
<ActivityIndicator color={accent} />
|
|
189
|
+
</View>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (feedItems.length === 0) {
|
|
194
|
+
return (
|
|
195
|
+
<View style={styles.centered}>
|
|
196
|
+
<Text style={[styles.emptyText, { color: p.textFaint }]}>
|
|
197
|
+
No feedback yet. Be the first to add one.
|
|
198
|
+
</Text>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
110
202
|
|
|
111
203
|
return (
|
|
112
204
|
<>
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<BottomSheetScrollView contentContainerStyle={styles.listBody}>
|
|
125
|
-
{feedItems.length === 0 ? (
|
|
126
|
-
<EmptyState p={p} message="No feedback yet. Be the first to add one." />
|
|
127
|
-
) : (
|
|
128
|
-
feedItems.map((f) => (
|
|
129
|
-
<FeedCard
|
|
130
|
-
key={f.id}
|
|
131
|
-
item={f}
|
|
132
|
-
p={p}
|
|
133
|
-
accent={accent}
|
|
134
|
-
onPress={() => onSelect(f.id)}
|
|
135
|
-
onVote={() => vote(f.id, "UP")}
|
|
136
|
-
/>
|
|
137
|
-
))
|
|
138
|
-
)}
|
|
139
|
-
</BottomSheetScrollView>
|
|
205
|
+
{feedItems.map((f) => (
|
|
206
|
+
<FeedCard
|
|
207
|
+
key={f.id}
|
|
208
|
+
item={f}
|
|
209
|
+
p={p}
|
|
210
|
+
accent={accent}
|
|
211
|
+
onPress={() => onSelect(f.id)}
|
|
212
|
+
onVote={() => vote(f.id, "UP")}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
140
215
|
</>
|
|
141
216
|
);
|
|
142
217
|
}
|
|
@@ -145,12 +220,10 @@ function FeedCard({
|
|
|
145
220
|
item, p, accent, onPress, onVote,
|
|
146
221
|
}: {
|
|
147
222
|
item: Feedback; p: Palette; accent: string;
|
|
148
|
-
onPress: () => void;
|
|
149
|
-
onVote: () => void;
|
|
223
|
+
onPress: () => void; onVote: () => void;
|
|
150
224
|
}) {
|
|
151
225
|
const voted = item.userVote === "UP";
|
|
152
226
|
const displayTitle = item.title ?? item.content;
|
|
153
|
-
const hasDescription = Boolean(item.title);
|
|
154
227
|
|
|
155
228
|
return (
|
|
156
229
|
<TouchableOpacity
|
|
@@ -164,7 +237,10 @@ function FeedCard({
|
|
|
164
237
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 4 }}
|
|
165
238
|
style={[
|
|
166
239
|
styles.voteBox,
|
|
167
|
-
{
|
|
240
|
+
{
|
|
241
|
+
borderColor: voted ? accent : p.border,
|
|
242
|
+
backgroundColor: voted ? accent + "1a" : p.bgSoft,
|
|
243
|
+
},
|
|
168
244
|
]}
|
|
169
245
|
>
|
|
170
246
|
<Text style={[styles.voteArrow, { color: accent }]}>▲</Text>
|
|
@@ -175,17 +251,15 @@ function FeedCard({
|
|
|
175
251
|
<Text style={[styles.cardTitle, { color: p.text }]} numberOfLines={2}>
|
|
176
252
|
{displayTitle}
|
|
177
253
|
</Text>
|
|
178
|
-
{
|
|
179
|
-
<Text style={[styles.
|
|
254
|
+
{item.title && (
|
|
255
|
+
<Text style={[styles.cardDesc, { color: p.textFaint }]} numberOfLines={1}>
|
|
180
256
|
{item.content}
|
|
181
257
|
</Text>
|
|
182
258
|
)}
|
|
183
259
|
<View style={styles.cardMeta}>
|
|
184
260
|
<TypeBadge type={item.type} p={p} />
|
|
185
261
|
<StatusDot status={item.status} />
|
|
186
|
-
<Text style={[styles.cardDate, { color: p.textFaint }]}>
|
|
187
|
-
{formatDate(item.createdAt)}
|
|
188
|
-
</Text>
|
|
262
|
+
<Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
|
|
189
263
|
</View>
|
|
190
264
|
</View>
|
|
191
265
|
|
|
@@ -194,26 +268,24 @@ function FeedCard({
|
|
|
194
268
|
);
|
|
195
269
|
}
|
|
196
270
|
|
|
197
|
-
// ───
|
|
271
|
+
// ─── FeedDetailBody ───────────────────────────────────────────────────────────
|
|
198
272
|
|
|
199
|
-
function
|
|
200
|
-
p, accent, feedbackId,
|
|
273
|
+
function FeedDetailBody({
|
|
274
|
+
p, accent, feedbackId,
|
|
201
275
|
}: {
|
|
202
|
-
p: Palette; accent: string; feedbackId: string;
|
|
276
|
+
p: Palette; accent: string; feedbackId: string;
|
|
203
277
|
}) {
|
|
204
278
|
const { vote, getItem } = useUpstep();
|
|
205
279
|
const [item, setItem] = useState<FeedbackWithComments | null>(null);
|
|
206
280
|
const [loading, setLoading] = useState(true);
|
|
207
281
|
const [voting, setVoting] = useState(false);
|
|
208
282
|
|
|
209
|
-
|
|
283
|
+
useEffect(() => {
|
|
210
284
|
let cancelled = false;
|
|
211
285
|
setLoading(true);
|
|
212
|
-
getItem(feedbackId)
|
|
213
|
-
if (!cancelled) { setItem(data); setLoading(false); }
|
|
214
|
-
|
|
215
|
-
if (!cancelled) setLoading(false);
|
|
216
|
-
});
|
|
286
|
+
getItem(feedbackId)
|
|
287
|
+
.then((data) => { if (!cancelled) { setItem(data); setLoading(false); } })
|
|
288
|
+
.catch(() => { if (!cancelled) setLoading(false); });
|
|
217
289
|
return () => { cancelled = true; };
|
|
218
290
|
}, [feedbackId, getItem]);
|
|
219
291
|
|
|
@@ -222,7 +294,6 @@ function FeedDetail({
|
|
|
222
294
|
setVoting(true);
|
|
223
295
|
try {
|
|
224
296
|
await vote(item.id, "UP");
|
|
225
|
-
// Refresh
|
|
226
297
|
const updated = await getItem(feedbackId);
|
|
227
298
|
setItem(updated);
|
|
228
299
|
} finally {
|
|
@@ -230,72 +301,60 @@ function FeedDetail({
|
|
|
230
301
|
}
|
|
231
302
|
}
|
|
232
303
|
|
|
304
|
+
if (loading || !item) {
|
|
305
|
+
return (
|
|
306
|
+
<View style={styles.centered}>
|
|
307
|
+
<ActivityIndicator color={accent} />
|
|
308
|
+
</View>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
233
312
|
return (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<BottomSheetView style={styles.centered}>
|
|
243
|
-
<ActivityIndicator color={accent} />
|
|
244
|
-
</BottomSheetView>
|
|
245
|
-
) : (
|
|
246
|
-
<BottomSheetScrollView contentContainerStyle={styles.detailBody}>
|
|
247
|
-
{/* Title + badges */}
|
|
248
|
-
{item.title && (
|
|
249
|
-
<Text style={[styles.detailTitle, { color: p.text }]}>{item.title}</Text>
|
|
250
|
-
)}
|
|
251
|
-
<View style={styles.detailBadgeRow}>
|
|
252
|
-
<TypeBadge type={item.type} p={p} />
|
|
253
|
-
<StatusBadge status={item.status} p={p} />
|
|
254
|
-
</View>
|
|
313
|
+
<View style={styles.detailPad}>
|
|
314
|
+
{item.title && (
|
|
315
|
+
<Text style={[styles.detailTitle, { color: p.text }]}>{item.title}</Text>
|
|
316
|
+
)}
|
|
317
|
+
<View style={styles.badgeRow}>
|
|
318
|
+
<TypeBadge type={item.type} p={p} />
|
|
319
|
+
<StatusBadge status={item.status} p={p} />
|
|
320
|
+
</View>
|
|
255
321
|
|
|
256
|
-
|
|
257
|
-
<Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
|
|
258
|
-
|
|
259
|
-
{/* Upvote */}
|
|
260
|
-
<TouchableOpacity
|
|
261
|
-
onPress={handleVote}
|
|
262
|
-
disabled={voting}
|
|
263
|
-
activeOpacity={0.8}
|
|
264
|
-
style={[
|
|
265
|
-
styles.upvoteBtn,
|
|
266
|
-
{
|
|
267
|
-
borderColor: item.userVote === "UP" ? accent : p.border,
|
|
268
|
-
backgroundColor: item.userVote === "UP" ? accent + "15" : p.bgSoft,
|
|
269
|
-
},
|
|
270
|
-
]}
|
|
271
|
-
>
|
|
272
|
-
{voting ? (
|
|
273
|
-
<ActivityIndicator size="small" color={accent} />
|
|
274
|
-
) : (
|
|
275
|
-
<>
|
|
276
|
-
<Text style={[styles.upvoteArrow, { color: accent }]}>▲</Text>
|
|
277
|
-
<Text style={[styles.upvoteCount, { color: p.text }]}>{item.upvotes}</Text>
|
|
278
|
-
<Text style={[styles.upvoteLabel, { color: p.textFaint }]}>
|
|
279
|
-
{item.userVote === "UP" ? "Upvoted" : "Upvote"}
|
|
280
|
-
</Text>
|
|
281
|
-
</>
|
|
282
|
-
)}
|
|
283
|
-
</TouchableOpacity>
|
|
322
|
+
<Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
|
|
284
323
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
<TouchableOpacity
|
|
325
|
+
onPress={handleVote}
|
|
326
|
+
disabled={voting}
|
|
327
|
+
activeOpacity={0.8}
|
|
328
|
+
style={[
|
|
329
|
+
styles.upvoteBtn,
|
|
330
|
+
{
|
|
331
|
+
borderColor: item.userVote === "UP" ? accent : p.border,
|
|
332
|
+
backgroundColor: item.userVote === "UP" ? accent + "15" : p.bgSoft,
|
|
333
|
+
},
|
|
334
|
+
]}
|
|
335
|
+
>
|
|
336
|
+
{voting ? (
|
|
337
|
+
<ActivityIndicator size="small" color={accent} />
|
|
338
|
+
) : (
|
|
339
|
+
<>
|
|
340
|
+
<Text style={[styles.upvoteArrow, { color: accent }]}>▲</Text>
|
|
341
|
+
<Text style={[styles.upvoteCount, { color: p.text }]}>{item.upvotes}</Text>
|
|
342
|
+
<Text style={[styles.upvoteLabel, { color: p.textFaint }]}>
|
|
343
|
+
{item.userVote === "UP" ? "Upvoted" : "Upvote"}
|
|
344
|
+
</Text>
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
</TouchableOpacity>
|
|
348
|
+
|
|
349
|
+
{item.comments.length > 0 && (
|
|
350
|
+
<View style={[styles.commentsSection, { borderTopColor: p.border }]}>
|
|
351
|
+
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>DEVELOPER RESPONSE</Text>
|
|
352
|
+
{item.comments.map((c) => (
|
|
353
|
+
<CommentBubble key={c.id} comment={c} p={p} accent={accent} />
|
|
354
|
+
))}
|
|
355
|
+
</View>
|
|
297
356
|
)}
|
|
298
|
-
|
|
357
|
+
</View>
|
|
299
358
|
);
|
|
300
359
|
}
|
|
301
360
|
|
|
@@ -308,16 +367,14 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
|
|
|
308
367
|
{comment.authorName ?? "Team"}
|
|
309
368
|
</Text>
|
|
310
369
|
</View>
|
|
311
|
-
<Text style={[styles.commentDate, { color: p.textFaint }]}>
|
|
312
|
-
{formatDate(comment.createdAt)}
|
|
313
|
-
</Text>
|
|
370
|
+
<Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
|
|
314
371
|
</View>
|
|
315
372
|
<Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
|
|
316
373
|
</View>
|
|
317
374
|
);
|
|
318
375
|
}
|
|
319
376
|
|
|
320
|
-
// ───
|
|
377
|
+
// ─── FeedCreateBody ───────────────────────────────────────────────────────────
|
|
321
378
|
|
|
322
379
|
const TYPES: FeedbackType[] = ["BUG", "FEATURE", "GENERAL"];
|
|
323
380
|
const TYPE_LABELS: Record<FeedbackType, string> = {
|
|
@@ -326,149 +383,120 @@ const TYPE_LABELS: Record<FeedbackType, string> = {
|
|
|
326
383
|
GENERAL: "General",
|
|
327
384
|
};
|
|
328
385
|
|
|
329
|
-
function
|
|
386
|
+
function FeedCreateBody({
|
|
387
|
+
p, accent, onDone,
|
|
388
|
+
}: {
|
|
389
|
+
p: Palette; accent: string; onDone: () => void;
|
|
390
|
+
}) {
|
|
330
391
|
const { submit } = useUpstep();
|
|
331
392
|
const [title, setTitle] = useState("");
|
|
332
393
|
const [description, setDescription] = useState("");
|
|
333
394
|
const [type, setType] = useState<FeedbackType>("GENERAL");
|
|
334
|
-
const [
|
|
395
|
+
const [submitting, setSubmitting] = useState(false);
|
|
335
396
|
const [success, setSuccess] = useState(false);
|
|
336
397
|
const [error, setError] = useState("");
|
|
337
398
|
|
|
338
399
|
async function handleSubmit() {
|
|
339
|
-
if (!title.trim() ||
|
|
340
|
-
|
|
400
|
+
if (!title.trim() || submitting) return;
|
|
401
|
+
setSubmitting(true);
|
|
341
402
|
setError("");
|
|
342
403
|
try {
|
|
343
404
|
await submit(title.trim(), description.trim() || title.trim(), type);
|
|
344
405
|
setSuccess(true);
|
|
345
|
-
|
|
346
|
-
setTimeout(onBack, 1400);
|
|
406
|
+
setTimeout(onDone, 1200);
|
|
347
407
|
} catch {
|
|
348
408
|
setError("Something went wrong. Please try again.");
|
|
349
409
|
} finally {
|
|
350
|
-
|
|
410
|
+
setSubmitting(false);
|
|
351
411
|
}
|
|
352
412
|
}
|
|
353
413
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
<BottomSheetScrollView
|
|
365
|
-
contentContainerStyle={styles.createBody}
|
|
366
|
-
keyboardShouldPersistTaps="handled"
|
|
367
|
-
>
|
|
368
|
-
{success ? (
|
|
369
|
-
<View style={styles.centered}>
|
|
370
|
-
<Text style={[styles.successTitle, { color: p.text }]}>Submitted</Text>
|
|
371
|
-
<Text style={[styles.successSub, { color: p.textFaint }]}>
|
|
372
|
-
Thanks — we received your feedback.
|
|
373
|
-
</Text>
|
|
374
|
-
</View>
|
|
375
|
-
) : (
|
|
376
|
-
<>
|
|
377
|
-
{/* Type selector */}
|
|
378
|
-
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>TYPE</Text>
|
|
379
|
-
<View style={styles.typeRow}>
|
|
380
|
-
{TYPES.map((t) => {
|
|
381
|
-
const active = type === t;
|
|
382
|
-
return (
|
|
383
|
-
<TouchableOpacity
|
|
384
|
-
key={t}
|
|
385
|
-
onPress={() => setType(t)}
|
|
386
|
-
activeOpacity={0.8}
|
|
387
|
-
style={[
|
|
388
|
-
styles.typeBtn,
|
|
389
|
-
{
|
|
390
|
-
borderColor: active ? accent : p.border,
|
|
391
|
-
backgroundColor: active ? accent : p.bg,
|
|
392
|
-
},
|
|
393
|
-
]}
|
|
394
|
-
>
|
|
395
|
-
<Text style={[styles.typeBtnText, { color: active ? "#fff" : p.textSoft }]}>
|
|
396
|
-
{TYPE_LABELS[t]}
|
|
397
|
-
</Text>
|
|
398
|
-
</TouchableOpacity>
|
|
399
|
-
);
|
|
400
|
-
})}
|
|
401
|
-
</View>
|
|
402
|
-
|
|
403
|
-
{/* Title */}
|
|
404
|
-
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>TITLE</Text>
|
|
405
|
-
<TextInput
|
|
406
|
-
value={title}
|
|
407
|
-
onChangeText={setTitle}
|
|
408
|
-
placeholder="Short summary of the issue or idea"
|
|
409
|
-
placeholderTextColor={p.textFaint}
|
|
410
|
-
maxLength={200}
|
|
411
|
-
returnKeyType="next"
|
|
412
|
-
style={[
|
|
413
|
-
styles.titleInput,
|
|
414
|
-
{ borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
|
|
415
|
-
]}
|
|
416
|
-
/>
|
|
417
|
-
|
|
418
|
-
{/* Description */}
|
|
419
|
-
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>DESCRIPTION</Text>
|
|
420
|
-
<TextInput
|
|
421
|
-
value={description}
|
|
422
|
-
onChangeText={setDescription}
|
|
423
|
-
placeholder="Steps to reproduce, expected behaviour, or more context (optional)"
|
|
424
|
-
placeholderTextColor={p.textFaint}
|
|
425
|
-
multiline
|
|
426
|
-
numberOfLines={5}
|
|
427
|
-
maxLength={2000}
|
|
428
|
-
textAlignVertical="top"
|
|
429
|
-
style={[
|
|
430
|
-
styles.descInput,
|
|
431
|
-
{ borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
|
|
432
|
-
]}
|
|
433
|
-
/>
|
|
434
|
-
|
|
435
|
-
{error ? (
|
|
436
|
-
<Text style={[styles.errorText, { color: "#d6453d" }]}>{error}</Text>
|
|
437
|
-
) : null}
|
|
414
|
+
if (success) {
|
|
415
|
+
return (
|
|
416
|
+
<View style={[styles.centered, { paddingTop: 40 }]}>
|
|
417
|
+
<Text style={[styles.successTitle, { color: p.text }]}>Submitted</Text>
|
|
418
|
+
<Text style={[styles.successSub, { color: p.textFaint }]}>
|
|
419
|
+
Thanks — we received your feedback.
|
|
420
|
+
</Text>
|
|
421
|
+
</View>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
438
424
|
|
|
425
|
+
return (
|
|
426
|
+
<View style={styles.createPad}>
|
|
427
|
+
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>TYPE</Text>
|
|
428
|
+
<View style={styles.typeRow}>
|
|
429
|
+
{TYPES.map((t) => {
|
|
430
|
+
const active = type === t;
|
|
431
|
+
return (
|
|
439
432
|
<TouchableOpacity
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
activeOpacity={0.
|
|
433
|
+
key={t}
|
|
434
|
+
onPress={() => setType(t)}
|
|
435
|
+
activeOpacity={0.8}
|
|
443
436
|
style={[
|
|
444
|
-
styles.
|
|
445
|
-
{
|
|
446
|
-
|
|
437
|
+
styles.typeBtn,
|
|
438
|
+
{
|
|
439
|
+
borderColor: active ? accent : p.border,
|
|
440
|
+
backgroundColor: active ? accent : p.bg,
|
|
441
|
+
},
|
|
447
442
|
]}
|
|
448
443
|
>
|
|
449
|
-
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<Text style={styles.submitBtnText}>Submit feedback</Text>
|
|
453
|
-
)}
|
|
444
|
+
<Text style={[styles.typeBtnText, { color: active ? "#fff" : p.textSoft }]}>
|
|
445
|
+
{TYPE_LABELS[t]}
|
|
446
|
+
</Text>
|
|
454
447
|
</TouchableOpacity>
|
|
455
|
-
|
|
456
|
-
)}
|
|
457
|
-
</
|
|
458
|
-
</>
|
|
459
|
-
);
|
|
460
|
-
}
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
450
|
+
</View>
|
|
461
451
|
|
|
462
|
-
|
|
452
|
+
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>TITLE</Text>
|
|
453
|
+
<TextInput
|
|
454
|
+
value={title}
|
|
455
|
+
onChangeText={setTitle}
|
|
456
|
+
placeholder="Short summary of the issue or idea"
|
|
457
|
+
placeholderTextColor={p.textFaint}
|
|
458
|
+
maxLength={200}
|
|
459
|
+
returnKeyType="next"
|
|
460
|
+
style={[styles.titleInput, { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text }]}
|
|
461
|
+
/>
|
|
462
|
+
|
|
463
|
+
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>DESCRIPTION</Text>
|
|
464
|
+
<TextInput
|
|
465
|
+
value={description}
|
|
466
|
+
onChangeText={setDescription}
|
|
467
|
+
placeholder="Steps to reproduce, expected behaviour, or more context (optional)"
|
|
468
|
+
placeholderTextColor={p.textFaint}
|
|
469
|
+
multiline
|
|
470
|
+
numberOfLines={5}
|
|
471
|
+
maxLength={2000}
|
|
472
|
+
textAlignVertical="top"
|
|
473
|
+
style={[styles.descInput, { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text }]}
|
|
474
|
+
/>
|
|
475
|
+
|
|
476
|
+
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
|
463
477
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
478
|
+
<TouchableOpacity
|
|
479
|
+
onPress={handleSubmit}
|
|
480
|
+
disabled={submitting || !title.trim()}
|
|
481
|
+
activeOpacity={0.85}
|
|
482
|
+
style={[
|
|
483
|
+
styles.submitBtn,
|
|
484
|
+
{ backgroundColor: accent },
|
|
485
|
+
(submitting || !title.trim()) && styles.submitBtnDisabled,
|
|
486
|
+
]}
|
|
487
|
+
>
|
|
488
|
+
{submitting ? (
|
|
489
|
+
<ActivityIndicator color="#fff" />
|
|
490
|
+
) : (
|
|
491
|
+
<Text style={styles.submitBtnText}>Submit feedback</Text>
|
|
492
|
+
)}
|
|
493
|
+
</TouchableOpacity>
|
|
468
494
|
</View>
|
|
469
495
|
);
|
|
470
496
|
}
|
|
471
497
|
|
|
498
|
+
// ─── Shared sub-components ────────────────────────────────────────────────────
|
|
499
|
+
|
|
472
500
|
const TYPE_BG: Record<FeedbackType, string> = {
|
|
473
501
|
BUG: "#fdecec", FEATURE: "#e9f0fd", GENERAL: "",
|
|
474
502
|
};
|
|
@@ -479,44 +507,64 @@ const TYPE_FG: Record<FeedbackType, string> = {
|
|
|
479
507
|
function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
|
|
480
508
|
return (
|
|
481
509
|
<View style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}>
|
|
482
|
-
<Text style={[styles.badgeText, { color: TYPE_FG[type] || p.textSoft }]}>
|
|
483
|
-
{type}
|
|
484
|
-
</Text>
|
|
510
|
+
<Text style={[styles.badgeText, { color: TYPE_FG[type] || p.textSoft }]}>{type}</Text>
|
|
485
511
|
</View>
|
|
486
512
|
);
|
|
487
513
|
}
|
|
488
514
|
|
|
489
|
-
const
|
|
490
|
-
OPEN: "#b45309",
|
|
491
|
-
|
|
492
|
-
DONE: "#15803d",
|
|
493
|
-
PENDING: "#ea580c",
|
|
494
|
-
CLOSED: "#6b7280",
|
|
515
|
+
const STATUS_COLOR: Record<string, string> = {
|
|
516
|
+
OPEN: "#b45309", IN_PROGRESS: "#1d4ed8", DONE: "#15803d",
|
|
517
|
+
PENDING: "#ea580c", CLOSED: "#6b7280",
|
|
495
518
|
};
|
|
496
519
|
|
|
497
520
|
function StatusDot({ status }: { status: string }) {
|
|
498
|
-
|
|
499
|
-
return <View style={[styles.statusDot, { backgroundColor: color }]} />;
|
|
521
|
+
return <View style={[styles.statusDot, { backgroundColor: STATUS_COLOR[status] ?? "#9ca3af" }]} />;
|
|
500
522
|
}
|
|
501
523
|
|
|
502
524
|
function StatusBadge({ status, p }: { status: string; p: Palette }) {
|
|
503
|
-
const label = status.replace("_", " ");
|
|
504
525
|
return (
|
|
505
526
|
<View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
|
|
506
527
|
<StatusDot status={status} />
|
|
507
|
-
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{
|
|
528
|
+
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{status.replace("_", " ")}</Text>
|
|
508
529
|
</View>
|
|
509
530
|
);
|
|
510
531
|
}
|
|
511
532
|
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
533
|
+
function fmtDate(iso: string) {
|
|
534
|
+
return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
515
535
|
}
|
|
516
536
|
|
|
517
537
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
518
538
|
|
|
519
539
|
const styles = StyleSheet.create({
|
|
540
|
+
// Modal overlay
|
|
541
|
+
overlay: {
|
|
542
|
+
flex: 1,
|
|
543
|
+
justifyContent: "flex-end",
|
|
544
|
+
},
|
|
545
|
+
backdrop: {
|
|
546
|
+
...StyleSheet.absoluteFill,
|
|
547
|
+
backgroundColor: "rgba(0,0,0,0.45)",
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
// Sheet container
|
|
551
|
+
sheet: {
|
|
552
|
+
maxHeight: "92%",
|
|
553
|
+
borderTopLeftRadius: 24,
|
|
554
|
+
borderTopRightRadius: 24,
|
|
555
|
+
overflow: "hidden",
|
|
556
|
+
},
|
|
557
|
+
handleWrap: {
|
|
558
|
+
alignItems: "center",
|
|
559
|
+
paddingTop: 10,
|
|
560
|
+
paddingBottom: 4,
|
|
561
|
+
},
|
|
562
|
+
handle: {
|
|
563
|
+
width: 40,
|
|
564
|
+
height: 4,
|
|
565
|
+
borderRadius: 2,
|
|
566
|
+
},
|
|
567
|
+
|
|
520
568
|
// Header
|
|
521
569
|
header: {
|
|
522
570
|
flexDirection: "row",
|
|
@@ -527,18 +575,18 @@ const styles = StyleSheet.create({
|
|
|
527
575
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
528
576
|
},
|
|
529
577
|
headerTitle: { fontSize: 16, fontWeight: "700", letterSpacing: -0.3 },
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
paddingHorizontal: 14,
|
|
534
|
-
paddingVertical: 7,
|
|
535
|
-
},
|
|
578
|
+
navBtn: { fontSize: 14, fontWeight: "600" },
|
|
579
|
+
navBtnSpacer: { width: 60 },
|
|
580
|
+
newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
|
|
536
581
|
newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
|
|
537
582
|
|
|
538
|
-
//
|
|
539
|
-
|
|
583
|
+
// Scroll
|
|
584
|
+
scroll: { flexGrow: 0 },
|
|
585
|
+
scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
|
|
540
586
|
|
|
541
|
-
//
|
|
587
|
+
// List
|
|
588
|
+
centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
|
|
589
|
+
emptyText: { fontSize: 14 },
|
|
542
590
|
card: {
|
|
543
591
|
flexDirection: "row",
|
|
544
592
|
alignItems: "center",
|
|
@@ -560,41 +608,29 @@ const styles = StyleSheet.create({
|
|
|
560
608
|
voteCount: { fontSize: 15, fontWeight: "700", lineHeight: 18 },
|
|
561
609
|
cardBody: { flex: 1 },
|
|
562
610
|
cardTitle: { fontSize: 14, fontWeight: "600", lineHeight: 19, marginBottom: 2 },
|
|
563
|
-
|
|
611
|
+
cardDesc: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
|
|
564
612
|
cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
565
613
|
cardDate: { fontSize: 11 },
|
|
566
614
|
chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
|
|
567
615
|
|
|
568
616
|
// Detail
|
|
569
|
-
|
|
617
|
+
detailPad: { paddingTop: 4 },
|
|
570
618
|
detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
|
|
571
|
-
|
|
619
|
+
badgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
|
|
572
620
|
detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
|
|
573
621
|
upvoteBtn: {
|
|
574
|
-
flexDirection: "row",
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
borderWidth: 1,
|
|
579
|
-
borderRadius: 12,
|
|
580
|
-
paddingHorizontal: 16,
|
|
581
|
-
paddingVertical: 10,
|
|
582
|
-
marginBottom: 8,
|
|
583
|
-
minWidth: 80,
|
|
584
|
-
justifyContent: "center",
|
|
622
|
+
flexDirection: "row", alignItems: "center", gap: 8,
|
|
623
|
+
alignSelf: "flex-start", borderWidth: 1, borderRadius: 12,
|
|
624
|
+
paddingHorizontal: 16, paddingVertical: 10, marginBottom: 8,
|
|
625
|
+
minWidth: 80, justifyContent: "center",
|
|
585
626
|
},
|
|
586
627
|
upvoteArrow: { fontSize: 13, lineHeight: 15 },
|
|
587
628
|
upvoteCount: { fontSize: 18, fontWeight: "700" },
|
|
588
629
|
upvoteLabel: { fontSize: 13, fontWeight: "500" },
|
|
589
|
-
|
|
590
|
-
// Comments
|
|
591
630
|
commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
|
|
592
631
|
commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
|
|
593
632
|
commentBubble: {
|
|
594
|
-
borderRadius: 12,
|
|
595
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
596
|
-
padding: 14,
|
|
597
|
-
marginBottom: 10,
|
|
633
|
+
borderRadius: 12, borderWidth: StyleSheet.hairlineWidth, padding: 14, marginBottom: 10,
|
|
598
634
|
},
|
|
599
635
|
commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
|
|
600
636
|
ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
|
|
@@ -603,38 +639,38 @@ const styles = StyleSheet.create({
|
|
|
603
639
|
commentContent: { fontSize: 14, lineHeight: 21 },
|
|
604
640
|
|
|
605
641
|
// Create
|
|
606
|
-
|
|
642
|
+
createPad: { paddingTop: 4 },
|
|
607
643
|
fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
|
|
608
644
|
typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
|
|
609
645
|
typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
|
|
610
646
|
typeBtnText: { fontSize: 13, fontWeight: "600" },
|
|
611
647
|
titleInput: {
|
|
612
|
-
borderWidth: 1, borderRadius: 12,
|
|
648
|
+
borderWidth: 1, borderRadius: 12,
|
|
649
|
+
paddingHorizontal: 14, paddingVertical: 12,
|
|
613
650
|
fontSize: 15, marginBottom: 20,
|
|
614
651
|
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
615
652
|
},
|
|
616
653
|
descInput: {
|
|
617
|
-
borderWidth: 1, borderRadius: 12,
|
|
654
|
+
borderWidth: 1, borderRadius: 12,
|
|
655
|
+
paddingHorizontal: 14, paddingVertical: 12,
|
|
618
656
|
fontSize: 14, minHeight: 110, marginBottom: 20,
|
|
619
657
|
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
620
658
|
},
|
|
621
|
-
errorText: { fontSize: 13, marginBottom: 12, fontWeight: "500" },
|
|
659
|
+
errorText: { fontSize: 13, marginBottom: 12, fontWeight: "500", color: "#d6453d" },
|
|
622
660
|
submitBtn: { borderRadius: 12, padding: 14, alignItems: "center" },
|
|
623
661
|
submitBtnDisabled: { opacity: 0.5 },
|
|
624
662
|
submitBtnText: { color: "#fff", fontSize: 15, fontWeight: "700" },
|
|
625
663
|
successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
|
|
626
664
|
successSub: { fontSize: 14, textAlign: "center" },
|
|
627
665
|
|
|
628
|
-
// Shared
|
|
629
|
-
empty: { alignItems: "center", paddingVertical: 60 },
|
|
630
|
-
emptyText: { fontSize: 14 },
|
|
631
|
-
centered: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: 60 },
|
|
666
|
+
// Shared badges
|
|
632
667
|
badge: { paddingHorizontal: 7, paddingVertical: 3, borderRadius: 6 },
|
|
633
668
|
badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
|
|
634
669
|
statusDot: { width: 6, height: 6, borderRadius: 3 },
|
|
635
670
|
statusBadge: {
|
|
636
|
-
flexDirection: "row", alignItems: "center", gap: 5,
|
|
637
|
-
|
|
671
|
+
flexDirection: "row", alignItems: "center", gap: 5,
|
|
672
|
+
paddingHorizontal: 8, paddingVertical: 3,
|
|
673
|
+
borderRadius: 6, borderWidth: StyleSheet.hairlineWidth,
|
|
638
674
|
},
|
|
639
675
|
statusBadgeText: { fontSize: 10, fontWeight: "600", textTransform: "capitalize" },
|
|
640
676
|
});
|