@upstep/react-native 0.6.3 → 0.6.7
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 +1 -1
- package/src/FeedbackContext.tsx +23 -0
- package/src/FeedbackSheet.tsx +94 -8
- package/src/api.ts +2 -1
package/package.json
CHANGED
package/src/FeedbackContext.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
useCallback,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { LayoutAnimation, Platform, UIManager } from "react-native";
|
|
9
10
|
import type {
|
|
10
11
|
Feedback,
|
|
11
12
|
FeedbackType,
|
|
@@ -15,6 +16,12 @@ import type {
|
|
|
15
16
|
} from "@upstep/types";
|
|
16
17
|
import { UpstepApiClient } from "./api";
|
|
17
18
|
|
|
19
|
+
// Enable LayoutAnimation on Android
|
|
20
|
+
if (Platform.OS === "android") {
|
|
21
|
+
const um = UIManager as unknown as { setLayoutAnimationEnabledExperimental?: (v: boolean) => void };
|
|
22
|
+
um.setLayoutAnimationEnabledExperimental?.(true);
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const DEFAULT_ACCENT = "#D97757";
|
|
19
26
|
|
|
20
27
|
interface UpstepContextValue {
|
|
@@ -74,6 +81,22 @@ export function FeedbackProvider({
|
|
|
74
81
|
);
|
|
75
82
|
|
|
76
83
|
const vote = useCallback(async (feedbackId: string, value: VoteValue) => {
|
|
84
|
+
// Optimistic update: flip vote + re-sort with a smooth animation
|
|
85
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
86
|
+
setFeedItems((prev) => {
|
|
87
|
+
const updated = prev.map((f) => {
|
|
88
|
+
if (f.id !== feedbackId) return f;
|
|
89
|
+
const wasVoted = f.userVote === "UP";
|
|
90
|
+
return {
|
|
91
|
+
...f,
|
|
92
|
+
upvotes: wasVoted ? f.upvotes - 1 : f.upvotes + 1,
|
|
93
|
+
userVote: wasVoted ? null : ("UP" as VoteValue),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return [...updated].sort((a, b) => b.upvotes - a.upvotes);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Confirm with server, then silently sync
|
|
77
100
|
await client.vote(feedbackId, value);
|
|
78
101
|
await loadFeed();
|
|
79
102
|
}, [client, loadFeed]);
|
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -66,6 +66,7 @@ export function FeedbackSheet() {
|
|
|
66
66
|
const p = isDark ? DARK : LIGHT;
|
|
67
67
|
|
|
68
68
|
const [nav, setNav] = useState<Nav>({ screen: "list" });
|
|
69
|
+
const [listTab, setListTab] = useState<"open" | "done">("open");
|
|
69
70
|
const insets = useSafeAreaInsets();
|
|
70
71
|
|
|
71
72
|
// Refresh list every time the modal opens
|
|
@@ -135,6 +136,30 @@ export function FeedbackSheet() {
|
|
|
135
136
|
)}
|
|
136
137
|
</View>
|
|
137
138
|
|
|
139
|
+
{/* Open / Completed tabs — only on list screen */}
|
|
140
|
+
{nav.screen === "list" && (
|
|
141
|
+
<View style={[styles.listTabBar, { borderBottomColor: p.border }]}>
|
|
142
|
+
<TouchableOpacity
|
|
143
|
+
onPress={() => setListTab("open")}
|
|
144
|
+
style={[styles.listTabBtn, listTab === "open" && { borderBottomColor: accentColor }]}
|
|
145
|
+
activeOpacity={0.7}
|
|
146
|
+
>
|
|
147
|
+
<Text style={[styles.listTabText, { color: listTab === "open" ? accentColor : p.textFaint }]}>
|
|
148
|
+
Open
|
|
149
|
+
</Text>
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
<TouchableOpacity
|
|
152
|
+
onPress={() => setListTab("done")}
|
|
153
|
+
style={[styles.listTabBtn, listTab === "done" && { borderBottomColor: accentColor }]}
|
|
154
|
+
activeOpacity={0.7}
|
|
155
|
+
>
|
|
156
|
+
<Text style={[styles.listTabText, { color: listTab === "done" ? accentColor : p.textFaint }]}>
|
|
157
|
+
Completed
|
|
158
|
+
</Text>
|
|
159
|
+
</TouchableOpacity>
|
|
160
|
+
</View>
|
|
161
|
+
)}
|
|
162
|
+
|
|
138
163
|
{/* Scrollable body */}
|
|
139
164
|
<ScrollView
|
|
140
165
|
style={styles.scroll}
|
|
@@ -146,6 +171,7 @@ export function FeedbackSheet() {
|
|
|
146
171
|
<FeedListBody
|
|
147
172
|
p={p}
|
|
148
173
|
accent={accentColor}
|
|
174
|
+
tab={listTab}
|
|
149
175
|
onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
|
|
150
176
|
/>
|
|
151
177
|
)}
|
|
@@ -189,12 +215,16 @@ export function FeedbackSheet() {
|
|
|
189
215
|
// ─── FeedListBody ─────────────────────────────────────────────────────────────
|
|
190
216
|
|
|
191
217
|
function FeedListBody({
|
|
192
|
-
p, accent, onSelect,
|
|
218
|
+
p, accent, tab, onSelect,
|
|
193
219
|
}: {
|
|
194
|
-
p: Palette; accent: string; onSelect: (id: string) => void;
|
|
220
|
+
p: Palette; accent: string; tab: "open" | "done"; onSelect: (id: string) => void;
|
|
195
221
|
}) {
|
|
196
222
|
const { feedItems, feedLoading, vote } = useUpstep();
|
|
197
223
|
|
|
224
|
+
const displayItems = tab === "done"
|
|
225
|
+
? feedItems.filter((f) => f.status === "DONE")
|
|
226
|
+
: feedItems.filter((f) => f.status === "OPEN" || f.status === "IN_PROGRESS" || f.status === "PENDING");
|
|
227
|
+
|
|
198
228
|
if (feedLoading) {
|
|
199
229
|
return (
|
|
200
230
|
<View style={styles.centered}>
|
|
@@ -203,11 +233,13 @@ function FeedListBody({
|
|
|
203
233
|
);
|
|
204
234
|
}
|
|
205
235
|
|
|
206
|
-
if (
|
|
236
|
+
if (displayItems.length === 0) {
|
|
207
237
|
return (
|
|
208
238
|
<View style={styles.centered}>
|
|
209
239
|
<Text style={[styles.emptyText, { color: p.textFaint }]}>
|
|
210
|
-
|
|
240
|
+
{tab === "done"
|
|
241
|
+
? "No completed items yet."
|
|
242
|
+
: "No open feedback yet. Be the first to add one."}
|
|
211
243
|
</Text>
|
|
212
244
|
</View>
|
|
213
245
|
);
|
|
@@ -215,7 +247,7 @@ function FeedListBody({
|
|
|
215
247
|
|
|
216
248
|
return (
|
|
217
249
|
<>
|
|
218
|
-
{
|
|
250
|
+
{displayItems.map((f) => (
|
|
219
251
|
<FeedCard
|
|
220
252
|
key={f.id}
|
|
221
253
|
item={f}
|
|
@@ -271,7 +303,9 @@ function FeedCard({
|
|
|
271
303
|
)}
|
|
272
304
|
<View style={styles.cardMeta}>
|
|
273
305
|
<TypeBadge type={item.type} p={p} />
|
|
274
|
-
|
|
306
|
+
{item.status === "PENDING"
|
|
307
|
+
? <PendingBadge />
|
|
308
|
+
: <StatusDot status={item.status} />}
|
|
275
309
|
<Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
|
|
276
310
|
</View>
|
|
277
311
|
</View>
|
|
@@ -329,7 +363,9 @@ function FeedDetailBody({
|
|
|
329
363
|
)}
|
|
330
364
|
<View style={styles.badgeRow}>
|
|
331
365
|
<TypeBadge type={item.type} p={p} />
|
|
332
|
-
|
|
366
|
+
{item.status === "PENDING"
|
|
367
|
+
? <PendingBadge />
|
|
368
|
+
: <StatusBadge status={item.status} p={p} />}
|
|
333
369
|
</View>
|
|
334
370
|
|
|
335
371
|
<Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
|
|
@@ -377,7 +413,7 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
|
|
|
377
413
|
<View style={styles.commentMeta}>
|
|
378
414
|
<View style={[styles.ownerBadge, { backgroundColor: accent + "20" }]}>
|
|
379
415
|
<Text style={[styles.ownerBadgeText, { color: accent }]}>
|
|
380
|
-
|
|
416
|
+
Developer
|
|
381
417
|
</Text>
|
|
382
418
|
</View>
|
|
383
419
|
<Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
|
|
@@ -517,6 +553,15 @@ const TYPE_FG: Record<FeedbackType, string> = {
|
|
|
517
553
|
BUG: "#d6453d", FEATURE: "#3b76d6", GENERAL: "",
|
|
518
554
|
};
|
|
519
555
|
|
|
556
|
+
function PendingBadge() {
|
|
557
|
+
return (
|
|
558
|
+
<View style={styles.pendingBadge}>
|
|
559
|
+
<View style={styles.pendingDot} />
|
|
560
|
+
<Text style={styles.pendingBadgeText}>Pending review</Text>
|
|
561
|
+
</View>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
520
565
|
function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
|
|
521
566
|
return (
|
|
522
567
|
<View style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}>
|
|
@@ -593,6 +638,23 @@ const styles = StyleSheet.create({
|
|
|
593
638
|
newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
|
|
594
639
|
newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
|
|
595
640
|
|
|
641
|
+
// List tab bar
|
|
642
|
+
listTabBar: {
|
|
643
|
+
flexDirection: "row",
|
|
644
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
645
|
+
},
|
|
646
|
+
listTabBtn: {
|
|
647
|
+
flex: 1,
|
|
648
|
+
alignItems: "center",
|
|
649
|
+
paddingVertical: 11,
|
|
650
|
+
borderBottomWidth: 2,
|
|
651
|
+
borderBottomColor: "transparent",
|
|
652
|
+
},
|
|
653
|
+
listTabText: {
|
|
654
|
+
fontSize: 13,
|
|
655
|
+
fontWeight: "600",
|
|
656
|
+
},
|
|
657
|
+
|
|
596
658
|
// Scroll — flex: 1 fills all remaining space after the fixed header
|
|
597
659
|
scroll: { flex: 1 },
|
|
598
660
|
scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
|
|
@@ -676,6 +738,30 @@ const styles = StyleSheet.create({
|
|
|
676
738
|
successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
|
|
677
739
|
successSub: { fontSize: 14, textAlign: "center" },
|
|
678
740
|
|
|
741
|
+
// Pending badge
|
|
742
|
+
pendingBadge: {
|
|
743
|
+
flexDirection: "row",
|
|
744
|
+
alignItems: "center",
|
|
745
|
+
gap: 4,
|
|
746
|
+
backgroundColor: "#fff7ed",
|
|
747
|
+
borderWidth: 1,
|
|
748
|
+
borderColor: "#fed7aa",
|
|
749
|
+
borderRadius: 6,
|
|
750
|
+
paddingHorizontal: 6,
|
|
751
|
+
paddingVertical: 2,
|
|
752
|
+
},
|
|
753
|
+
pendingDot: {
|
|
754
|
+
width: 5,
|
|
755
|
+
height: 5,
|
|
756
|
+
borderRadius: 3,
|
|
757
|
+
backgroundColor: "#ea580c",
|
|
758
|
+
},
|
|
759
|
+
pendingBadgeText: {
|
|
760
|
+
fontSize: 10,
|
|
761
|
+
fontWeight: "700",
|
|
762
|
+
color: "#ea580c",
|
|
763
|
+
},
|
|
764
|
+
|
|
679
765
|
// Branding
|
|
680
766
|
poweredBy: {
|
|
681
767
|
alignItems: "center",
|
package/src/api.ts
CHANGED
|
@@ -40,7 +40,8 @@ export class UpstepApiClient {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async getItem(feedbackId: string): Promise<FeedbackWithComments> {
|
|
43
|
-
const
|
|
43
|
+
const qs = this.userId ? `?endUserId=${encodeURIComponent(this.userId)}` : "";
|
|
44
|
+
const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}${qs}`, {
|
|
44
45
|
headers: this.headers(),
|
|
45
46
|
});
|
|
46
47
|
if (!res.ok) throw new Error(`Upstep: ${res.status}`);
|