@upstep/react-native 0.6.6 → 0.6.8
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 +32 -3
- package/src/FeedbackSheet.tsx +54 -5
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,9 +81,31 @@ export function FeedbackProvider({
|
|
|
74
81
|
);
|
|
75
82
|
|
|
76
83
|
const vote = useCallback(async (feedbackId: string, value: VoteValue) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
// Capture snapshot for rollback, then animate into new sorted position
|
|
85
|
+
let snapshot: Feedback[] = [];
|
|
86
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
87
|
+
setFeedItems((prev) => {
|
|
88
|
+
snapshot = prev;
|
|
89
|
+
const updated = prev.map((f) => {
|
|
90
|
+
if (f.id !== feedbackId) return f;
|
|
91
|
+
const wasVoted = f.userVote === "UP";
|
|
92
|
+
return {
|
|
93
|
+
...f,
|
|
94
|
+
upvotes: wasVoted ? f.upvotes - 1 : f.upvotes + 1,
|
|
95
|
+
userVote: wasVoted ? null : ("UP" as VoteValue),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
return [...updated].sort((a, b) => b.upvotes - a.upvotes);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await client.vote(feedbackId, value);
|
|
103
|
+
} catch {
|
|
104
|
+
// API failed — animate back to original order
|
|
105
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
106
|
+
setFeedItems(snapshot);
|
|
107
|
+
}
|
|
108
|
+
}, [client]);
|
|
80
109
|
|
|
81
110
|
const getItem = useCallback(
|
|
82
111
|
(feedbackId: string) => client.getItem(feedbackId),
|
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}
|
|
@@ -606,6 +638,23 @@ const styles = StyleSheet.create({
|
|
|
606
638
|
newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
|
|
607
639
|
newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
|
|
608
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
|
+
|
|
609
658
|
// Scroll — flex: 1 fills all remaining space after the fixed header
|
|
610
659
|
scroll: { flex: 1 },
|
|
611
660
|
scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
|