@upstep/react-native 0.6.16 → 0.7.1
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/CHANGELOG.md +11 -0
- package/README.md +16 -1
- package/package.json +8 -3
- package/src/FeedbackContext.tsx +82 -32
- package/src/FeedbackSheet.tsx +453 -124
- package/src/SupportView.tsx +641 -0
- package/src/api.ts +3 -0
- package/src/index.ts +17 -1
- package/.turbo/turbo-type-check.log +0 -4
- package/tsconfig.json +0 -9
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
Linking,
|
|
16
16
|
} from "react-native";
|
|
17
17
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
Comment,
|
|
20
|
+
Feedback,
|
|
21
|
+
FeedbackType,
|
|
22
|
+
FeedbackWithComments,
|
|
23
|
+
} from "@upstep/types";
|
|
24
|
+
import { SupportView } from "./SupportView";
|
|
19
25
|
import { useUpstep } from "./FeedbackContext";
|
|
20
26
|
|
|
21
27
|
// ─── Theme ────────────────────────────────────────────────────────────────────
|
|
@@ -32,7 +38,7 @@ interface Palette {
|
|
|
32
38
|
|
|
33
39
|
const LIGHT: Palette = {
|
|
34
40
|
bg: "#ffffff",
|
|
35
|
-
bgSoft: "#
|
|
41
|
+
bgSoft: "#f5f5f5",
|
|
36
42
|
text: "#1a1915",
|
|
37
43
|
textSoft: "#56544d",
|
|
38
44
|
textFaint: "#9b9890",
|
|
@@ -41,8 +47,8 @@ const LIGHT: Palette = {
|
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
const DARK: Palette = {
|
|
44
|
-
bg: "#
|
|
45
|
-
bgSoft: "#
|
|
50
|
+
bg: "#000000",
|
|
51
|
+
bgSoft: "#171717",
|
|
46
52
|
text: "#f5f4f0",
|
|
47
53
|
textSoft: "#b4b1a8",
|
|
48
54
|
textFaint: "#7d7a72",
|
|
@@ -53,6 +59,7 @@ const DARK: Palette = {
|
|
|
53
59
|
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
54
60
|
|
|
55
61
|
type Nav =
|
|
62
|
+
| { screen: "support" }
|
|
56
63
|
| { screen: "list" }
|
|
57
64
|
| { screen: "detail"; feedbackId: string }
|
|
58
65
|
| { screen: "create" };
|
|
@@ -60,7 +67,17 @@ type Nav =
|
|
|
60
67
|
// ─── FeedbackSheet ────────────────────────────────────────────────────────────
|
|
61
68
|
|
|
62
69
|
export function FeedbackSheet() {
|
|
63
|
-
const {
|
|
70
|
+
const {
|
|
71
|
+
client,
|
|
72
|
+
isOpen,
|
|
73
|
+
closeSheet,
|
|
74
|
+
accentColor,
|
|
75
|
+
theme,
|
|
76
|
+
loadFeed,
|
|
77
|
+
showBranding,
|
|
78
|
+
supportConfig,
|
|
79
|
+
supportRevision,
|
|
80
|
+
} = useUpstep();
|
|
64
81
|
const scheme = useColorScheme();
|
|
65
82
|
const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
|
|
66
83
|
const p = isDark ? DARK : LIGHT;
|
|
@@ -70,13 +87,14 @@ export function FeedbackSheet() {
|
|
|
70
87
|
|
|
71
88
|
// Refresh list every time the modal opens
|
|
72
89
|
useEffect(() => {
|
|
73
|
-
if (isOpen)
|
|
74
|
-
|
|
90
|
+
if (isOpen) {
|
|
91
|
+
loadFeed();
|
|
92
|
+
setNav({ screen: supportConfig?.enabled ? "support" : "list" });
|
|
93
|
+
}
|
|
94
|
+
}, [isOpen, loadFeed, supportConfig?.enabled]);
|
|
75
95
|
|
|
76
96
|
function handleClose() {
|
|
77
97
|
closeSheet();
|
|
78
|
-
// Reset to list when closed so next open starts fresh
|
|
79
|
-
setTimeout(() => setNav({ screen: "list" }), 300);
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
return (
|
|
@@ -103,9 +121,51 @@ export function FeedbackSheet() {
|
|
|
103
121
|
|
|
104
122
|
{/* Fixed header */}
|
|
105
123
|
<View style={[styles.header, { borderBottomColor: p.border }]}>
|
|
106
|
-
{
|
|
124
|
+
{supportConfig ? (
|
|
125
|
+
<>
|
|
126
|
+
<View style={{ flexDirection: "row", gap: 8, flex: 1 }}>
|
|
127
|
+
{["support", "list"].map((screen) => (
|
|
128
|
+
<TouchableOpacity
|
|
129
|
+
key={screen}
|
|
130
|
+
onPress={() => setNav({ screen } as Nav)}
|
|
131
|
+
style={{
|
|
132
|
+
paddingVertical: 10,
|
|
133
|
+
paddingHorizontal: 14,
|
|
134
|
+
borderRadius: 10,
|
|
135
|
+
backgroundColor: (
|
|
136
|
+
screen === "support"
|
|
137
|
+
? nav.screen === "support"
|
|
138
|
+
: nav.screen !== "support"
|
|
139
|
+
)
|
|
140
|
+
? p.bgSoft
|
|
141
|
+
: "transparent",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<Text
|
|
145
|
+
style={{
|
|
146
|
+
fontSize: 16,
|
|
147
|
+
fontWeight: "600",
|
|
148
|
+
color: p.text,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
{screen === "support" ? "Support" : "Feedback"}
|
|
152
|
+
</Text>
|
|
153
|
+
</TouchableOpacity>
|
|
154
|
+
))}
|
|
155
|
+
</View>
|
|
156
|
+
<TouchableOpacity
|
|
157
|
+
accessibilityLabel="Close"
|
|
158
|
+
onPress={handleClose}
|
|
159
|
+
style={{ padding: 10 }}
|
|
160
|
+
>
|
|
161
|
+
<Text style={{ fontSize: 20, color: p.textSoft }}>×</Text>
|
|
162
|
+
</TouchableOpacity>
|
|
163
|
+
</>
|
|
164
|
+
) : nav.screen === "list" ? (
|
|
107
165
|
<>
|
|
108
|
-
<Text style={[styles.headerTitle, { color: p.text }]}>
|
|
166
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>
|
|
167
|
+
Feedback
|
|
168
|
+
</Text>
|
|
109
169
|
<TouchableOpacity
|
|
110
170
|
onPress={() => setNav({ screen: "create" })}
|
|
111
171
|
activeOpacity={0.8}
|
|
@@ -120,9 +180,13 @@ export function FeedbackSheet() {
|
|
|
120
180
|
onPress={() => setNav({ screen: "list" })}
|
|
121
181
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
122
182
|
>
|
|
123
|
-
<Text style={[styles.navBtn, { color: accentColor }]}>
|
|
183
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>
|
|
184
|
+
Cancel
|
|
185
|
+
</Text>
|
|
124
186
|
</TouchableOpacity>
|
|
125
|
-
<Text style={[styles.headerTitle, { color: p.text }]}>
|
|
187
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>
|
|
188
|
+
New feedback
|
|
189
|
+
</Text>
|
|
126
190
|
<View style={styles.navBtnSpacer} />
|
|
127
191
|
</>
|
|
128
192
|
) : (
|
|
@@ -130,51 +194,90 @@ export function FeedbackSheet() {
|
|
|
130
194
|
onPress={() => setNav({ screen: "list" })}
|
|
131
195
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
132
196
|
>
|
|
133
|
-
<Text style={[styles.navBtn, { color: accentColor }]}
|
|
197
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>
|
|
198
|
+
← Back
|
|
199
|
+
</Text>
|
|
134
200
|
</TouchableOpacity>
|
|
135
201
|
)}
|
|
136
202
|
</View>
|
|
137
203
|
|
|
138
204
|
{/* Scrollable body */}
|
|
139
|
-
|
|
140
|
-
style={
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
{nav.screen === "support" && supportConfig ? (
|
|
206
|
+
<View style={{ flex: 1, paddingBottom: insets.bottom }}>
|
|
207
|
+
{isOpen && (
|
|
208
|
+
<SupportView
|
|
209
|
+
key={supportRevision}
|
|
210
|
+
client={client.support}
|
|
211
|
+
config={supportConfig}
|
|
212
|
+
accentColor={accentColor}
|
|
213
|
+
dark={isDark}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</View>
|
|
217
|
+
) : (
|
|
218
|
+
<ScrollView
|
|
219
|
+
style={styles.scroll}
|
|
220
|
+
contentContainerStyle={styles.scrollContent}
|
|
221
|
+
keyboardShouldPersistTaps="handled"
|
|
222
|
+
showsVerticalScrollIndicator={false}
|
|
223
|
+
>
|
|
224
|
+
{supportConfig && nav.screen === "list" && (
|
|
225
|
+
<TouchableOpacity
|
|
226
|
+
onPress={() => setNav({ screen: "create" })}
|
|
227
|
+
style={[
|
|
228
|
+
styles.newBtn,
|
|
229
|
+
{
|
|
230
|
+
backgroundColor: accentColor,
|
|
231
|
+
alignSelf: "flex-end",
|
|
232
|
+
marginBottom: 12,
|
|
233
|
+
},
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
236
|
+
<Text style={styles.newBtnText}>+ New feedback</Text>
|
|
237
|
+
</TouchableOpacity>
|
|
238
|
+
)}
|
|
239
|
+
{nav.screen === "list" && (
|
|
240
|
+
<FeedListBody
|
|
241
|
+
p={p}
|
|
242
|
+
accent={accentColor}
|
|
243
|
+
onSelect={(id) =>
|
|
244
|
+
setNav({ screen: "detail", feedbackId: id })
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
{nav.screen === "detail" && "feedbackId" in nav && (
|
|
249
|
+
<FeedDetailBody
|
|
250
|
+
p={p}
|
|
251
|
+
accent={accentColor}
|
|
252
|
+
feedbackId={
|
|
253
|
+
(nav as { screen: "detail"; feedbackId: string }).feedbackId
|
|
254
|
+
}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
{nav.screen === "create" && (
|
|
258
|
+
<FeedCreateBody
|
|
259
|
+
p={p}
|
|
260
|
+
accent={accentColor}
|
|
261
|
+
onDone={() => {
|
|
262
|
+
setNav({ screen: "list" });
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
</ScrollView>
|
|
267
|
+
)}
|
|
169
268
|
|
|
170
269
|
{/* Branding — hidden for Business plan */}
|
|
171
|
-
{showBranding && (
|
|
270
|
+
{showBranding && nav.screen !== "support" && (
|
|
172
271
|
<TouchableOpacity
|
|
173
272
|
onPress={() => Linking.openURL("https://upstep.dev")}
|
|
174
273
|
activeOpacity={0.6}
|
|
175
274
|
style={[
|
|
176
275
|
styles.poweredBy,
|
|
177
|
-
{
|
|
276
|
+
{
|
|
277
|
+
borderTopColor: p.border,
|
|
278
|
+
paddingBottom: insets.bottom + 10,
|
|
279
|
+
backgroundColor: p.bg,
|
|
280
|
+
},
|
|
178
281
|
]}
|
|
179
282
|
>
|
|
180
283
|
<Text style={[styles.poweredByText, { color: p.textFaint }]}>
|
|
@@ -191,9 +294,13 @@ export function FeedbackSheet() {
|
|
|
191
294
|
// ─── FeedListBody ─────────────────────────────────────────────────────────────
|
|
192
295
|
|
|
193
296
|
function FeedListBody({
|
|
194
|
-
p,
|
|
297
|
+
p,
|
|
298
|
+
accent,
|
|
299
|
+
onSelect,
|
|
195
300
|
}: {
|
|
196
|
-
p: Palette;
|
|
301
|
+
p: Palette;
|
|
302
|
+
accent: string;
|
|
303
|
+
onSelect: (id: string) => void;
|
|
197
304
|
}) {
|
|
198
305
|
const { feedItems, feedLoading, vote } = useUpstep();
|
|
199
306
|
|
|
@@ -234,10 +341,17 @@ function FeedListBody({
|
|
|
234
341
|
}
|
|
235
342
|
|
|
236
343
|
function FeedCard({
|
|
237
|
-
item,
|
|
344
|
+
item,
|
|
345
|
+
p,
|
|
346
|
+
accent,
|
|
347
|
+
onPress,
|
|
348
|
+
onVote,
|
|
238
349
|
}: {
|
|
239
|
-
item: Feedback;
|
|
240
|
-
|
|
350
|
+
item: Feedback;
|
|
351
|
+
p: Palette;
|
|
352
|
+
accent: string;
|
|
353
|
+
onPress: () => void;
|
|
354
|
+
onVote: () => void;
|
|
241
355
|
}) {
|
|
242
356
|
const voted = item.userVote === "UP";
|
|
243
357
|
const displayTitle = item.title ?? item.content;
|
|
@@ -260,8 +374,12 @@ function FeedCard({
|
|
|
260
374
|
},
|
|
261
375
|
]}
|
|
262
376
|
>
|
|
263
|
-
<Text style={[styles.voteArrow, { color: voted ? "#fff" : accent }]}
|
|
264
|
-
|
|
377
|
+
<Text style={[styles.voteArrow, { color: voted ? "#fff" : accent }]}>
|
|
378
|
+
▲
|
|
379
|
+
</Text>
|
|
380
|
+
<Text style={[styles.voteCount, { color: voted ? "#fff" : p.text }]}>
|
|
381
|
+
{item.upvotes}
|
|
382
|
+
</Text>
|
|
265
383
|
</TouchableOpacity>
|
|
266
384
|
|
|
267
385
|
<View style={styles.cardBody}>
|
|
@@ -269,16 +387,23 @@ function FeedCard({
|
|
|
269
387
|
{displayTitle}
|
|
270
388
|
</Text>
|
|
271
389
|
{item.title && (
|
|
272
|
-
<Text
|
|
390
|
+
<Text
|
|
391
|
+
style={[styles.cardDesc, { color: p.textFaint }]}
|
|
392
|
+
numberOfLines={1}
|
|
393
|
+
>
|
|
273
394
|
{item.content}
|
|
274
395
|
</Text>
|
|
275
396
|
)}
|
|
276
397
|
<View style={styles.cardMeta}>
|
|
277
398
|
<TypeBadge type={item.type} p={p} />
|
|
278
|
-
{item.status === "PENDING"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
399
|
+
{item.status === "PENDING" ? (
|
|
400
|
+
<PendingBadge />
|
|
401
|
+
) : (
|
|
402
|
+
<StatusDot status={item.status} color={item.boardStatus?.color} />
|
|
403
|
+
)}
|
|
404
|
+
<Text style={[styles.cardDate, { color: p.textFaint }]}>
|
|
405
|
+
{fmtDate(item.createdAt)}
|
|
406
|
+
</Text>
|
|
282
407
|
</View>
|
|
283
408
|
<LabelBadges labels={item.labels} p={p} />
|
|
284
409
|
</View>
|
|
@@ -291,9 +416,13 @@ function FeedCard({
|
|
|
291
416
|
// ─── FeedDetailBody ───────────────────────────────────────────────────────────
|
|
292
417
|
|
|
293
418
|
function FeedDetailBody({
|
|
294
|
-
p,
|
|
419
|
+
p,
|
|
420
|
+
accent,
|
|
421
|
+
feedbackId,
|
|
295
422
|
}: {
|
|
296
|
-
p: Palette;
|
|
423
|
+
p: Palette;
|
|
424
|
+
accent: string;
|
|
425
|
+
feedbackId: string;
|
|
297
426
|
}) {
|
|
298
427
|
const { vote, getItem } = useUpstep();
|
|
299
428
|
const [item, setItem] = useState<FeedbackWithComments | null>(null);
|
|
@@ -304,9 +433,18 @@ function FeedDetailBody({
|
|
|
304
433
|
let cancelled = false;
|
|
305
434
|
setLoading(true);
|
|
306
435
|
getItem(feedbackId)
|
|
307
|
-
.then((data) => {
|
|
308
|
-
|
|
309
|
-
|
|
436
|
+
.then((data) => {
|
|
437
|
+
if (!cancelled) {
|
|
438
|
+
setItem(data);
|
|
439
|
+
setLoading(false);
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
.catch(() => {
|
|
443
|
+
if (!cancelled) setLoading(false);
|
|
444
|
+
});
|
|
445
|
+
return () => {
|
|
446
|
+
cancelled = true;
|
|
447
|
+
};
|
|
310
448
|
}, [feedbackId, getItem]);
|
|
311
449
|
|
|
312
450
|
async function handleVote() {
|
|
@@ -332,17 +470,27 @@ function FeedDetailBody({
|
|
|
332
470
|
return (
|
|
333
471
|
<View style={styles.detailPad}>
|
|
334
472
|
{item.title && (
|
|
335
|
-
<Text style={[styles.detailTitle, { color: p.text }]}>
|
|
473
|
+
<Text style={[styles.detailTitle, { color: p.text }]}>
|
|
474
|
+
{item.title}
|
|
475
|
+
</Text>
|
|
336
476
|
)}
|
|
337
477
|
<View style={styles.badgeRow}>
|
|
338
478
|
<TypeBadge type={item.type} p={p} />
|
|
339
|
-
{item.status === "PENDING"
|
|
340
|
-
|
|
341
|
-
|
|
479
|
+
{item.status === "PENDING" ? (
|
|
480
|
+
<PendingBadge />
|
|
481
|
+
) : (
|
|
482
|
+
<StatusBadge
|
|
483
|
+
status={item.status}
|
|
484
|
+
boardStatus={item.boardStatus}
|
|
485
|
+
p={p}
|
|
486
|
+
/>
|
|
487
|
+
)}
|
|
342
488
|
<LabelBadges labels={item.labels} p={p} />
|
|
343
489
|
</View>
|
|
344
490
|
|
|
345
|
-
<Text style={[styles.detailContent, { color: p.textSoft }]}>
|
|
491
|
+
<Text style={[styles.detailContent, { color: p.textSoft }]}>
|
|
492
|
+
{item.content}
|
|
493
|
+
</Text>
|
|
346
494
|
|
|
347
495
|
<TouchableOpacity
|
|
348
496
|
onPress={handleVote}
|
|
@@ -361,7 +509,9 @@ function FeedDetailBody({
|
|
|
361
509
|
) : (
|
|
362
510
|
<>
|
|
363
511
|
<Text style={[styles.upvoteArrow, { color: accent }]}>▲</Text>
|
|
364
|
-
<Text style={[styles.upvoteCount, { color: p.text }]}>
|
|
512
|
+
<Text style={[styles.upvoteCount, { color: p.text }]}>
|
|
513
|
+
{item.upvotes}
|
|
514
|
+
</Text>
|
|
365
515
|
<Text style={[styles.upvoteLabel, { color: p.textFaint }]}>
|
|
366
516
|
{item.userVote === "UP" ? "Upvoted" : "Upvote"}
|
|
367
517
|
</Text>
|
|
@@ -371,7 +521,9 @@ function FeedDetailBody({
|
|
|
371
521
|
|
|
372
522
|
{item.comments.length > 0 && (
|
|
373
523
|
<View style={[styles.commentsSection, { borderTopColor: p.border }]}>
|
|
374
|
-
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>
|
|
524
|
+
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>
|
|
525
|
+
COMMENTS
|
|
526
|
+
</Text>
|
|
375
527
|
{item.comments.map((c) => (
|
|
376
528
|
<CommentBubble key={c.id} comment={c} p={p} accent={accent} />
|
|
377
529
|
))}
|
|
@@ -381,18 +533,35 @@ function FeedDetailBody({
|
|
|
381
533
|
);
|
|
382
534
|
}
|
|
383
535
|
|
|
384
|
-
function CommentBubble({
|
|
536
|
+
function CommentBubble({
|
|
537
|
+
comment,
|
|
538
|
+
p,
|
|
539
|
+
accent,
|
|
540
|
+
}: {
|
|
541
|
+
comment: Comment;
|
|
542
|
+
p: Palette;
|
|
543
|
+
accent: string;
|
|
544
|
+
}) {
|
|
385
545
|
return (
|
|
386
|
-
<View
|
|
546
|
+
<View
|
|
547
|
+
style={[
|
|
548
|
+
styles.commentBubble,
|
|
549
|
+
{ backgroundColor: p.bgSoft, borderColor: p.border },
|
|
550
|
+
]}
|
|
551
|
+
>
|
|
387
552
|
<View style={styles.commentMeta}>
|
|
388
553
|
<View style={[styles.ownerBadge, { backgroundColor: accent + "20" }]}>
|
|
389
554
|
<Text style={[styles.ownerBadgeText, { color: accent }]}>
|
|
390
|
-
|
|
555
|
+
{comment.authorName?.trim() || "UpStep Team"}
|
|
391
556
|
</Text>
|
|
392
557
|
</View>
|
|
393
|
-
<Text style={[styles.commentDate, { color: p.textFaint }]}>
|
|
558
|
+
<Text style={[styles.commentDate, { color: p.textFaint }]}>
|
|
559
|
+
{fmtDate(comment.createdAt)}
|
|
560
|
+
</Text>
|
|
394
561
|
</View>
|
|
395
|
-
<Text style={[styles.commentContent, { color: p.textSoft }]}>
|
|
562
|
+
<Text style={[styles.commentContent, { color: p.textSoft }]}>
|
|
563
|
+
{comment.content}
|
|
564
|
+
</Text>
|
|
396
565
|
</View>
|
|
397
566
|
);
|
|
398
567
|
}
|
|
@@ -407,9 +576,13 @@ const TYPE_LABELS: Record<FeedbackType, string> = {
|
|
|
407
576
|
};
|
|
408
577
|
|
|
409
578
|
function FeedCreateBody({
|
|
410
|
-
p,
|
|
579
|
+
p,
|
|
580
|
+
accent,
|
|
581
|
+
onDone,
|
|
411
582
|
}: {
|
|
412
|
-
p: Palette;
|
|
583
|
+
p: Palette;
|
|
584
|
+
accent: string;
|
|
585
|
+
onDone: () => void;
|
|
413
586
|
}) {
|
|
414
587
|
const { submit } = useUpstep();
|
|
415
588
|
const [title, setTitle] = useState("");
|
|
@@ -464,7 +637,12 @@ function FeedCreateBody({
|
|
|
464
637
|
},
|
|
465
638
|
]}
|
|
466
639
|
>
|
|
467
|
-
<Text
|
|
640
|
+
<Text
|
|
641
|
+
style={[
|
|
642
|
+
styles.typeBtnText,
|
|
643
|
+
{ color: active ? "#fff" : p.textSoft },
|
|
644
|
+
]}
|
|
645
|
+
>
|
|
468
646
|
{TYPE_LABELS[t]}
|
|
469
647
|
</Text>
|
|
470
648
|
</TouchableOpacity>
|
|
@@ -480,10 +658,15 @@ function FeedCreateBody({
|
|
|
480
658
|
placeholderTextColor={p.textFaint}
|
|
481
659
|
maxLength={200}
|
|
482
660
|
returnKeyType="next"
|
|
483
|
-
style={[
|
|
661
|
+
style={[
|
|
662
|
+
styles.titleInput,
|
|
663
|
+
{ borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
|
|
664
|
+
]}
|
|
484
665
|
/>
|
|
485
666
|
|
|
486
|
-
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>
|
|
667
|
+
<Text style={[styles.fieldLabel, { color: p.textFaint }]}>
|
|
668
|
+
DESCRIPTION
|
|
669
|
+
</Text>
|
|
487
670
|
<TextInput
|
|
488
671
|
value={description}
|
|
489
672
|
onChangeText={setDescription}
|
|
@@ -493,7 +676,10 @@ function FeedCreateBody({
|
|
|
493
676
|
numberOfLines={5}
|
|
494
677
|
maxLength={2000}
|
|
495
678
|
textAlignVertical="top"
|
|
496
|
-
style={[
|
|
679
|
+
style={[
|
|
680
|
+
styles.descInput,
|
|
681
|
+
{ borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
|
|
682
|
+
]}
|
|
497
683
|
/>
|
|
498
684
|
|
|
499
685
|
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
|
@@ -521,10 +707,14 @@ function FeedCreateBody({
|
|
|
521
707
|
// ─── Shared sub-components ────────────────────────────────────────────────────
|
|
522
708
|
|
|
523
709
|
const TYPE_BG: Record<FeedbackType, string> = {
|
|
524
|
-
BUG: "#fdecec",
|
|
710
|
+
BUG: "#fdecec",
|
|
711
|
+
FEATURE: "#e9f0fd",
|
|
712
|
+
GENERAL: "",
|
|
525
713
|
};
|
|
526
714
|
const TYPE_FG: Record<FeedbackType, string> = {
|
|
527
|
-
BUG: "#d6453d",
|
|
715
|
+
BUG: "#d6453d",
|
|
716
|
+
FEATURE: "#3b76d6",
|
|
717
|
+
GENERAL: "",
|
|
528
718
|
};
|
|
529
719
|
|
|
530
720
|
function PendingBadge() {
|
|
@@ -538,43 +728,91 @@ function PendingBadge() {
|
|
|
538
728
|
|
|
539
729
|
function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
|
|
540
730
|
return (
|
|
541
|
-
<View
|
|
542
|
-
|
|
731
|
+
<View
|
|
732
|
+
style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}
|
|
733
|
+
>
|
|
734
|
+
<Text style={[styles.badgeText, { color: TYPE_FG[type] || p.textSoft }]}>
|
|
735
|
+
{type}
|
|
736
|
+
</Text>
|
|
543
737
|
</View>
|
|
544
738
|
);
|
|
545
739
|
}
|
|
546
740
|
|
|
547
741
|
const STATUS_COLOR: Record<string, string> = {
|
|
548
|
-
OPEN: "#b45309",
|
|
549
|
-
|
|
742
|
+
OPEN: "#b45309",
|
|
743
|
+
IN_PROGRESS: "#1d4ed8",
|
|
744
|
+
DONE: "#15803d",
|
|
745
|
+
PENDING: "#ea580c",
|
|
746
|
+
CLOSED: "#6b7280",
|
|
550
747
|
};
|
|
551
748
|
|
|
552
|
-
function StatusDot({
|
|
553
|
-
|
|
749
|
+
function StatusDot({
|
|
750
|
+
status,
|
|
751
|
+
color,
|
|
752
|
+
}: {
|
|
753
|
+
status: string;
|
|
754
|
+
color?: string | undefined;
|
|
755
|
+
}) {
|
|
756
|
+
return (
|
|
757
|
+
<View
|
|
758
|
+
style={[
|
|
759
|
+
styles.statusDot,
|
|
760
|
+
{ backgroundColor: color ?? STATUS_COLOR[status] ?? "#9ca3af" },
|
|
761
|
+
]}
|
|
762
|
+
/>
|
|
763
|
+
);
|
|
554
764
|
}
|
|
555
765
|
|
|
556
766
|
function StatusBadge({
|
|
557
|
-
status,
|
|
767
|
+
status,
|
|
768
|
+
boardStatus,
|
|
769
|
+
p,
|
|
558
770
|
}: {
|
|
559
|
-
status: string;
|
|
771
|
+
status: string;
|
|
772
|
+
boardStatus: Feedback["boardStatus"];
|
|
773
|
+
p: Palette;
|
|
560
774
|
}) {
|
|
561
775
|
return (
|
|
562
|
-
<View
|
|
776
|
+
<View
|
|
777
|
+
style={[
|
|
778
|
+
styles.statusBadge,
|
|
779
|
+
{ backgroundColor: p.bgSoft, borderColor: p.border },
|
|
780
|
+
]}
|
|
781
|
+
>
|
|
563
782
|
<StatusDot status={status} color={boardStatus?.color} />
|
|
564
|
-
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
|
|
783
|
+
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
|
|
784
|
+
{boardStatus?.name ?? status.replace("_", " ")}
|
|
785
|
+
</Text>
|
|
565
786
|
</View>
|
|
566
787
|
);
|
|
567
788
|
}
|
|
568
789
|
|
|
569
|
-
function LabelBadges({
|
|
790
|
+
function LabelBadges({
|
|
791
|
+
labels,
|
|
792
|
+
p,
|
|
793
|
+
}: {
|
|
794
|
+
labels: Feedback["labels"];
|
|
795
|
+
p: Palette;
|
|
796
|
+
}) {
|
|
570
797
|
if (!labels?.length) return null;
|
|
571
798
|
|
|
572
799
|
return (
|
|
573
800
|
<View style={styles.labelBadges}>
|
|
574
801
|
{labels.map((label) => (
|
|
575
|
-
<View
|
|
802
|
+
<View
|
|
803
|
+
key={label.id}
|
|
804
|
+
style={[
|
|
805
|
+
styles.labelBadge,
|
|
806
|
+
{ backgroundColor: p.bgSoft, borderColor: label.color },
|
|
807
|
+
]}
|
|
808
|
+
>
|
|
576
809
|
<View style={[styles.labelDot, { backgroundColor: label.color }]} />
|
|
577
|
-
<Text
|
|
810
|
+
<Text
|
|
811
|
+
style={[styles.labelBadgeText, { color: p.textSoft }]}
|
|
812
|
+
numberOfLines={1}
|
|
813
|
+
>
|
|
814
|
+
{label.name}
|
|
815
|
+
</Text>
|
|
578
816
|
</View>
|
|
579
817
|
))}
|
|
580
818
|
</View>
|
|
@@ -582,7 +820,10 @@ function LabelBadges({ labels, p }: { labels: Feedback["labels"]; p: Palette })
|
|
|
582
820
|
}
|
|
583
821
|
|
|
584
822
|
function fmtDate(iso: string) {
|
|
585
|
-
return new Date(iso).toLocaleDateString(undefined, {
|
|
823
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
824
|
+
month: "short",
|
|
825
|
+
day: "numeric",
|
|
826
|
+
});
|
|
586
827
|
}
|
|
587
828
|
|
|
588
829
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
@@ -640,7 +881,11 @@ const styles = StyleSheet.create({
|
|
|
640
881
|
scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
|
|
641
882
|
|
|
642
883
|
// List
|
|
643
|
-
centered: {
|
|
884
|
+
centered: {
|
|
885
|
+
alignItems: "center",
|
|
886
|
+
justifyContent: "center",
|
|
887
|
+
paddingVertical: 60,
|
|
888
|
+
},
|
|
644
889
|
emptyText: { fontSize: 14 },
|
|
645
890
|
card: {
|
|
646
891
|
flexDirection: "row",
|
|
@@ -662,36 +907,85 @@ const styles = StyleSheet.create({
|
|
|
662
907
|
voteArrow: { fontSize: 11, lineHeight: 13 },
|
|
663
908
|
voteCount: { fontSize: 15, fontWeight: "700", lineHeight: 18 },
|
|
664
909
|
cardBody: { flex: 1 },
|
|
665
|
-
cardTitle: {
|
|
910
|
+
cardTitle: {
|
|
911
|
+
fontSize: 14,
|
|
912
|
+
fontWeight: "600",
|
|
913
|
+
lineHeight: 19,
|
|
914
|
+
marginBottom: 2,
|
|
915
|
+
},
|
|
666
916
|
cardDesc: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
|
|
667
917
|
cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
668
918
|
cardDate: { fontSize: 11 },
|
|
669
919
|
labelBadges: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 7 },
|
|
670
|
-
labelBadge: {
|
|
920
|
+
labelBadge: {
|
|
921
|
+
flexDirection: "row",
|
|
922
|
+
alignItems: "center",
|
|
923
|
+
gap: 5,
|
|
924
|
+
borderWidth: 1,
|
|
925
|
+
borderRadius: 10,
|
|
926
|
+
paddingHorizontal: 7,
|
|
927
|
+
paddingVertical: 3,
|
|
928
|
+
maxWidth: "100%",
|
|
929
|
+
},
|
|
671
930
|
labelDot: { width: 6, height: 6, borderRadius: 3 },
|
|
672
931
|
labelBadgeText: { fontSize: 11, fontWeight: "500", flexShrink: 1 },
|
|
673
932
|
chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
|
|
674
933
|
|
|
675
934
|
// Detail
|
|
676
935
|
detailPad: { paddingTop: 4 },
|
|
677
|
-
detailTitle: {
|
|
678
|
-
|
|
936
|
+
detailTitle: {
|
|
937
|
+
fontSize: 22,
|
|
938
|
+
fontWeight: "700",
|
|
939
|
+
letterSpacing: -0.4,
|
|
940
|
+
marginBottom: 10,
|
|
941
|
+
lineHeight: 28,
|
|
942
|
+
},
|
|
943
|
+
badgeRow: {
|
|
944
|
+
flexDirection: "row",
|
|
945
|
+
flexWrap: "wrap",
|
|
946
|
+
gap: 8,
|
|
947
|
+
marginBottom: 16,
|
|
948
|
+
},
|
|
679
949
|
detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
|
|
680
950
|
upvoteBtn: {
|
|
681
|
-
flexDirection: "row",
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
951
|
+
flexDirection: "row",
|
|
952
|
+
alignItems: "center",
|
|
953
|
+
gap: 8,
|
|
954
|
+
alignSelf: "flex-start",
|
|
955
|
+
borderWidth: 1,
|
|
956
|
+
borderRadius: 12,
|
|
957
|
+
paddingHorizontal: 16,
|
|
958
|
+
paddingVertical: 10,
|
|
959
|
+
marginBottom: 8,
|
|
960
|
+
minWidth: 80,
|
|
961
|
+
justifyContent: "center",
|
|
685
962
|
},
|
|
686
963
|
upvoteArrow: { fontSize: 13, lineHeight: 15 },
|
|
687
964
|
upvoteCount: { fontSize: 18, fontWeight: "700" },
|
|
688
965
|
upvoteLabel: { fontSize: 13, fontWeight: "500" },
|
|
689
|
-
commentsSection: {
|
|
690
|
-
|
|
966
|
+
commentsSection: {
|
|
967
|
+
marginTop: 28,
|
|
968
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
969
|
+
paddingTop: 20,
|
|
970
|
+
},
|
|
971
|
+
commentsLabel: {
|
|
972
|
+
fontSize: 10,
|
|
973
|
+
fontWeight: "700",
|
|
974
|
+
letterSpacing: 0.8,
|
|
975
|
+
marginBottom: 12,
|
|
976
|
+
},
|
|
691
977
|
commentBubble: {
|
|
692
|
-
borderRadius: 12,
|
|
978
|
+
borderRadius: 12,
|
|
979
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
980
|
+
padding: 14,
|
|
981
|
+
marginBottom: 10,
|
|
982
|
+
},
|
|
983
|
+
commentMeta: {
|
|
984
|
+
flexDirection: "row",
|
|
985
|
+
alignItems: "center",
|
|
986
|
+
gap: 8,
|
|
987
|
+
marginBottom: 8,
|
|
693
988
|
},
|
|
694
|
-
commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
|
|
695
989
|
ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
|
|
696
990
|
ownerBadgeText: { fontSize: 11, fontWeight: "700" },
|
|
697
991
|
commentDate: { fontSize: 11 },
|
|
@@ -699,27 +993,54 @@ const styles = StyleSheet.create({
|
|
|
699
993
|
|
|
700
994
|
// Create
|
|
701
995
|
createPad: { paddingTop: 4 },
|
|
702
|
-
fieldLabel: {
|
|
996
|
+
fieldLabel: {
|
|
997
|
+
fontSize: 10,
|
|
998
|
+
fontWeight: "700",
|
|
999
|
+
letterSpacing: 0.8,
|
|
1000
|
+
marginBottom: 8,
|
|
1001
|
+
},
|
|
703
1002
|
typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
|
|
704
|
-
typeBtn: {
|
|
1003
|
+
typeBtn: {
|
|
1004
|
+
paddingVertical: 9,
|
|
1005
|
+
paddingHorizontal: 14,
|
|
1006
|
+
borderRadius: 10,
|
|
1007
|
+
borderWidth: 1,
|
|
1008
|
+
},
|
|
705
1009
|
typeBtnText: { fontSize: 13, fontWeight: "600" },
|
|
706
1010
|
titleInput: {
|
|
707
|
-
borderWidth: 1,
|
|
708
|
-
|
|
709
|
-
|
|
1011
|
+
borderWidth: 1,
|
|
1012
|
+
borderRadius: 12,
|
|
1013
|
+
paddingHorizontal: 14,
|
|
1014
|
+
paddingVertical: 12,
|
|
1015
|
+
fontSize: 15,
|
|
1016
|
+
marginBottom: 20,
|
|
710
1017
|
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
711
1018
|
},
|
|
712
1019
|
descInput: {
|
|
713
|
-
borderWidth: 1,
|
|
714
|
-
|
|
715
|
-
|
|
1020
|
+
borderWidth: 1,
|
|
1021
|
+
borderRadius: 12,
|
|
1022
|
+
paddingHorizontal: 14,
|
|
1023
|
+
paddingVertical: 12,
|
|
1024
|
+
fontSize: 14,
|
|
1025
|
+
minHeight: 110,
|
|
1026
|
+
marginBottom: 20,
|
|
716
1027
|
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
717
1028
|
},
|
|
718
|
-
errorText: {
|
|
1029
|
+
errorText: {
|
|
1030
|
+
fontSize: 13,
|
|
1031
|
+
marginBottom: 12,
|
|
1032
|
+
fontWeight: "500",
|
|
1033
|
+
color: "#d6453d",
|
|
1034
|
+
},
|
|
719
1035
|
submitBtn: { borderRadius: 12, padding: 14, alignItems: "center" },
|
|
720
1036
|
submitBtnDisabled: { opacity: 0.5 },
|
|
721
1037
|
submitBtnText: { color: "#fff", fontSize: 15, fontWeight: "700" },
|
|
722
|
-
successTitle: {
|
|
1038
|
+
successTitle: {
|
|
1039
|
+
fontSize: 20,
|
|
1040
|
+
fontWeight: "700",
|
|
1041
|
+
textAlign: "center",
|
|
1042
|
+
marginBottom: 6,
|
|
1043
|
+
},
|
|
723
1044
|
successSub: { fontSize: 14, textAlign: "center" },
|
|
724
1045
|
|
|
725
1046
|
// Pending badge
|
|
@@ -759,9 +1080,17 @@ const styles = StyleSheet.create({
|
|
|
759
1080
|
badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
|
|
760
1081
|
statusDot: { width: 6, height: 6, borderRadius: 3 },
|
|
761
1082
|
statusBadge: {
|
|
762
|
-
flexDirection: "row",
|
|
763
|
-
|
|
764
|
-
|
|
1083
|
+
flexDirection: "row",
|
|
1084
|
+
alignItems: "center",
|
|
1085
|
+
gap: 5,
|
|
1086
|
+
paddingHorizontal: 8,
|
|
1087
|
+
paddingVertical: 3,
|
|
1088
|
+
borderRadius: 6,
|
|
1089
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1090
|
+
},
|
|
1091
|
+
statusBadgeText: {
|
|
1092
|
+
fontSize: 10,
|
|
1093
|
+
fontWeight: "600",
|
|
1094
|
+
textTransform: "capitalize",
|
|
765
1095
|
},
|
|
766
|
-
statusBadgeText: { fontSize: 10, fontWeight: "600", textTransform: "capitalize" },
|
|
767
1096
|
});
|