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