@upstep/react-native 0.3.0 → 0.5.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 -2
- package/src/FeedbackButton.tsx +14 -6
- package/src/FeedbackContext.tsx +41 -12
- package/src/FeedbackSheet.tsx +590 -129
- package/src/api.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstep/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Upstep feedback SDK for React Native",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@upstep/types": "0.
|
|
15
|
+
"@upstep/types": "0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"@gorhom/bottom-sheet": ">=4",
|
package/src/FeedbackButton.tsx
CHANGED
|
@@ -8,14 +8,19 @@ interface Props {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function FeedbackButton({ position = "bottom-right", label = "Feedback" }: Props) {
|
|
11
|
-
const { openSheet } = useUpstep();
|
|
11
|
+
const { openSheet, accentColor } = useUpstep();
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<TouchableOpacity
|
|
15
15
|
onPress={openSheet}
|
|
16
|
-
style={[
|
|
16
|
+
style={[
|
|
17
|
+
styles.btn,
|
|
18
|
+
{ backgroundColor: accentColor },
|
|
19
|
+
position === "bottom-left" ? styles.left : styles.right,
|
|
20
|
+
]}
|
|
17
21
|
activeOpacity={0.85}
|
|
18
22
|
>
|
|
23
|
+
<Text style={styles.icon}>💬</Text>
|
|
19
24
|
<Text style={styles.label}>{label}</Text>
|
|
20
25
|
</TouchableOpacity>
|
|
21
26
|
);
|
|
@@ -25,18 +30,21 @@ const styles = StyleSheet.create({
|
|
|
25
30
|
btn: {
|
|
26
31
|
position: "absolute",
|
|
27
32
|
bottom: 24,
|
|
28
|
-
|
|
33
|
+
flexDirection: "row",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
gap: 6,
|
|
29
36
|
borderRadius: 9999,
|
|
30
37
|
paddingVertical: 12,
|
|
31
|
-
paddingHorizontal:
|
|
38
|
+
paddingHorizontal: 18,
|
|
32
39
|
shadowColor: "#000",
|
|
33
|
-
shadowOffset: { width: 0, height:
|
|
40
|
+
shadowOffset: { width: 0, height: 5 },
|
|
34
41
|
shadowOpacity: 0.2,
|
|
35
|
-
shadowRadius:
|
|
42
|
+
shadowRadius: 10,
|
|
36
43
|
elevation: 6,
|
|
37
44
|
zIndex: 100,
|
|
38
45
|
},
|
|
39
46
|
right: { right: 20 },
|
|
40
47
|
left: { left: 20 },
|
|
48
|
+
icon: { fontSize: 14 },
|
|
41
49
|
label: { color: "#fff", fontSize: 14, fontWeight: "600" },
|
|
42
50
|
});
|
package/src/FeedbackContext.tsx
CHANGED
|
@@ -7,21 +7,32 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from "react";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
Feedback,
|
|
12
|
+
FeedbackType,
|
|
13
|
+
FeedbackWithComments,
|
|
14
|
+
UpstepConfig,
|
|
15
|
+
VoteValue,
|
|
16
|
+
} from "@upstep/types";
|
|
11
17
|
import { UpstepApiClient } from "./api";
|
|
12
18
|
import type BottomSheet from "@gorhom/bottom-sheet";
|
|
13
19
|
|
|
20
|
+
const DEFAULT_ACCENT = "#D97757";
|
|
21
|
+
|
|
14
22
|
interface UpstepContextValue {
|
|
15
23
|
client: UpstepApiClient;
|
|
16
24
|
feedItems: Feedback[];
|
|
17
25
|
loadFeed: () => Promise<void>;
|
|
18
|
-
submit: (content: string, type?: FeedbackType) => Promise<void>;
|
|
26
|
+
submit: (title: string, content: string, type?: FeedbackType) => Promise<void>;
|
|
19
27
|
vote: (feedbackId: string, value: VoteValue) => Promise<void>;
|
|
28
|
+
getItem: (feedbackId: string) => Promise<FeedbackWithComments>;
|
|
20
29
|
/** Open the feedback sheet — call from your own UI (settings, menu, etc.). */
|
|
21
30
|
openSheet: () => void;
|
|
22
31
|
/** Set / update the end-user id at runtime (e.g. after the user logs in). */
|
|
23
32
|
identify: (userId: string | undefined) => void;
|
|
24
33
|
sheetRef: React.RefObject<BottomSheet | null>;
|
|
34
|
+
accentColor: string;
|
|
35
|
+
theme: "light" | "dark" | "auto";
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
const UpstepContext = createContext<UpstepContextValue | null>(null);
|
|
@@ -34,14 +45,13 @@ export function FeedbackProvider({
|
|
|
34
45
|
const [feedItems, setFeedItems] = useState<Feedback[]>([]);
|
|
35
46
|
const sheetRef = useRef<BottomSheet>(null);
|
|
36
47
|
|
|
37
|
-
// Keep the end-user id in sync if `userId` resolves after mount (async login).
|
|
38
48
|
useEffect(() => {
|
|
39
49
|
client.setUserId(config.userId);
|
|
40
50
|
}, [client, config.userId]);
|
|
41
51
|
|
|
42
52
|
const loadFeed = useCallback(async () => {
|
|
43
53
|
try {
|
|
44
|
-
const data = await client.listFeedback({ sort: "votes", limit:
|
|
54
|
+
const data = await client.listFeedback({ sort: "votes", limit: 30 });
|
|
45
55
|
setFeedItems(data.items);
|
|
46
56
|
} catch {
|
|
47
57
|
// non-critical
|
|
@@ -50,26 +60,45 @@ export function FeedbackProvider({
|
|
|
50
60
|
|
|
51
61
|
useEffect(() => { loadFeed(); }, [loadFeed]);
|
|
52
62
|
|
|
53
|
-
const submit = useCallback(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
const submit = useCallback(
|
|
64
|
+
async (title: string, content: string, type?: FeedbackType) => {
|
|
65
|
+
const payload: Parameters<typeof client.submitFeedback>[0] = { content };
|
|
66
|
+
if (title.trim()) payload.title = title.trim();
|
|
67
|
+
if (type !== undefined) payload.type = type;
|
|
68
|
+
await client.submitFeedback(payload);
|
|
69
|
+
await loadFeed();
|
|
70
|
+
},
|
|
71
|
+
[client, loadFeed]
|
|
72
|
+
);
|
|
59
73
|
|
|
60
74
|
const vote = useCallback(async (feedbackId: string, value: VoteValue) => {
|
|
61
75
|
await client.vote(feedbackId, value);
|
|
62
76
|
await loadFeed();
|
|
63
77
|
}, [client, loadFeed]);
|
|
64
78
|
|
|
79
|
+
const getItem = useCallback(
|
|
80
|
+
(feedbackId: string) => client.getItem(feedbackId),
|
|
81
|
+
[client]
|
|
82
|
+
);
|
|
83
|
+
|
|
65
84
|
const openSheet = useCallback(() => {
|
|
66
85
|
sheetRef.current?.expand();
|
|
67
86
|
}, []);
|
|
68
87
|
|
|
69
|
-
const identify = useCallback(
|
|
88
|
+
const identify = useCallback(
|
|
89
|
+
(userId: string | undefined) => client.setUserId(userId),
|
|
90
|
+
[client]
|
|
91
|
+
);
|
|
70
92
|
|
|
71
93
|
return (
|
|
72
|
-
<UpstepContext.Provider
|
|
94
|
+
<UpstepContext.Provider
|
|
95
|
+
value={{
|
|
96
|
+
client, feedItems, loadFeed, submit, vote, getItem,
|
|
97
|
+
openSheet, identify, sheetRef,
|
|
98
|
+
accentColor: config.accentColor ?? DEFAULT_ACCENT,
|
|
99
|
+
theme: config.theme ?? "auto",
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
73
102
|
{children}
|
|
74
103
|
</UpstepContext.Provider>
|
|
75
104
|
);
|
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -1,43 +1,78 @@
|
|
|
1
|
-
import React, { useState, useMemo } from "react";
|
|
1
|
+
import React, { useState, useMemo, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
5
|
TextInput,
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
StyleSheet,
|
|
8
|
-
ScrollView,
|
|
9
8
|
ActivityIndicator,
|
|
9
|
+
ScrollView,
|
|
10
|
+
useColorScheme,
|
|
11
|
+
Platform,
|
|
10
12
|
} from "react-native";
|
|
11
13
|
import BottomSheet, { BottomSheetView, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
|
12
|
-
import type { Feedback, FeedbackType } from "@upstep/types";
|
|
14
|
+
import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
|
|
13
15
|
import { useUpstep } from "./FeedbackContext";
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// ─── Theme ────────────────────────────────────────────────────────────────────
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
interface Palette {
|
|
20
|
+
bg: string;
|
|
21
|
+
bgSoft: string;
|
|
22
|
+
bgHover: string;
|
|
23
|
+
text: string;
|
|
24
|
+
textSoft: string;
|
|
25
|
+
textFaint: string;
|
|
26
|
+
border: string;
|
|
27
|
+
handle: string;
|
|
28
|
+
shadow: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LIGHT: Palette = {
|
|
32
|
+
bg: "#ffffff",
|
|
33
|
+
bgSoft: "#f6f5f2",
|
|
34
|
+
bgHover: "#efede8",
|
|
35
|
+
text: "#1a1915",
|
|
36
|
+
textSoft: "#56544d",
|
|
37
|
+
textFaint: "#9b9890",
|
|
38
|
+
border: "#e8e6df",
|
|
39
|
+
handle: "#dad7ce",
|
|
40
|
+
shadow: "#00000014",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DARK: Palette = {
|
|
44
|
+
bg: "#1c1b19",
|
|
45
|
+
bgSoft: "#262522",
|
|
46
|
+
bgHover: "#302e2a",
|
|
47
|
+
text: "#f5f4f0",
|
|
48
|
+
textSoft: "#b4b1a8",
|
|
49
|
+
textFaint: "#7d7a72",
|
|
50
|
+
border: "#33312c",
|
|
51
|
+
handle: "#48453f",
|
|
52
|
+
shadow: "#0000003a",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
type Nav =
|
|
58
|
+
| { screen: "list" }
|
|
59
|
+
| { screen: "detail"; feedbackId: string }
|
|
60
|
+
| { screen: "create" };
|
|
61
|
+
|
|
62
|
+
// ─── FeedbackSheet ────────────────────────────────────────────────────────────
|
|
18
63
|
|
|
19
64
|
export function FeedbackSheet() {
|
|
20
|
-
const {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const [type, setType] = useState<FeedbackType>("GENERAL");
|
|
25
|
-
const [msg, setMsg] = useState("");
|
|
26
|
-
const [loading, setLoading] = useState(false);
|
|
65
|
+
const { sheetRef, accentColor, theme } = useUpstep();
|
|
66
|
+
const scheme = useColorScheme();
|
|
67
|
+
const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
|
|
68
|
+
const p = isDark ? DARK : LIGHT;
|
|
27
69
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setMsg("Thanks for your feedback!");
|
|
35
|
-
} catch {
|
|
36
|
-
setMsg("Something went wrong. Please try again.");
|
|
37
|
-
} finally {
|
|
38
|
-
setLoading(false);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
70
|
+
const [nav, setNav] = useState<Nav>({ screen: "list" });
|
|
71
|
+
const snapPoints = useMemo(() => ["90%"], []);
|
|
72
|
+
|
|
73
|
+
const goList = useCallback(() => setNav({ screen: "list" }), []);
|
|
74
|
+
const goDetail = useCallback((id: string) => setNav({ screen: "detail", feedbackId: id }), []);
|
|
75
|
+
const goCreate = useCallback(() => setNav({ screen: "create" }), []);
|
|
41
76
|
|
|
42
77
|
return (
|
|
43
78
|
<BottomSheet
|
|
@@ -45,135 +80,561 @@ export function FeedbackSheet() {
|
|
|
45
80
|
index={-1}
|
|
46
81
|
snapPoints={snapPoints}
|
|
47
82
|
enablePanDownToClose
|
|
48
|
-
backgroundStyle={
|
|
49
|
-
handleIndicatorStyle={
|
|
83
|
+
backgroundStyle={{ backgroundColor: p.bg, borderRadius: 24 }}
|
|
84
|
+
handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
|
|
85
|
+
onClose={() => setNav({ screen: "list" })}
|
|
50
86
|
>
|
|
51
|
-
|
|
52
|
-
<
|
|
87
|
+
{nav.screen === "list" && (
|
|
88
|
+
<FeedList p={p} accent={accentColor} onSelect={goDetail} onCreate={goCreate} />
|
|
89
|
+
)}
|
|
90
|
+
{nav.screen === "detail" && (
|
|
91
|
+
<FeedDetail p={p} accent={accentColor} feedbackId={nav.feedbackId} onBack={goList} />
|
|
92
|
+
)}
|
|
93
|
+
{nav.screen === "create" && (
|
|
94
|
+
<FeedCreate p={p} accent={accentColor} onBack={goList} />
|
|
95
|
+
)}
|
|
96
|
+
</BottomSheet>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── FeedList ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function FeedList({
|
|
103
|
+
p, accent, onSelect, onCreate,
|
|
104
|
+
}: {
|
|
105
|
+
p: Palette; accent: string;
|
|
106
|
+
onSelect: (id: string) => void;
|
|
107
|
+
onCreate: () => void;
|
|
108
|
+
}) {
|
|
109
|
+
const { feedItems, vote } = useUpstep();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
<BottomSheetView style={[styles.header, { borderBottomColor: p.border }]}>
|
|
114
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
|
|
115
|
+
<TouchableOpacity
|
|
116
|
+
onPress={onCreate}
|
|
117
|
+
activeOpacity={0.8}
|
|
118
|
+
style={[styles.newBtn, { backgroundColor: accent }]}
|
|
119
|
+
>
|
|
120
|
+
<Text style={styles.newBtnText}>+ New</Text>
|
|
121
|
+
</TouchableOpacity>
|
|
53
122
|
</BottomSheetView>
|
|
54
123
|
|
|
55
|
-
<
|
|
56
|
-
{
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function FeedCard({
|
|
145
|
+
item, p, accent, onPress, onVote,
|
|
146
|
+
}: {
|
|
147
|
+
item: Feedback; p: Palette; accent: string;
|
|
148
|
+
onPress: () => void;
|
|
149
|
+
onVote: () => void;
|
|
150
|
+
}) {
|
|
151
|
+
const voted = item.userVote === "UP";
|
|
152
|
+
const displayTitle = item.title ?? item.content;
|
|
153
|
+
const hasDescription = Boolean(item.title);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<TouchableOpacity
|
|
157
|
+
onPress={onPress}
|
|
158
|
+
activeOpacity={0.85}
|
|
159
|
+
style={[styles.card, { borderColor: p.border, backgroundColor: p.bg }]}
|
|
160
|
+
>
|
|
161
|
+
<TouchableOpacity
|
|
162
|
+
onPress={onVote}
|
|
163
|
+
activeOpacity={0.8}
|
|
164
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 4 }}
|
|
165
|
+
style={[
|
|
166
|
+
styles.voteBox,
|
|
167
|
+
{ borderColor: voted ? accent : p.border, backgroundColor: voted ? accent + "1a" : p.bgSoft },
|
|
168
|
+
]}
|
|
169
|
+
>
|
|
170
|
+
<Text style={[styles.voteArrow, { color: accent }]}>▲</Text>
|
|
171
|
+
<Text style={[styles.voteCount, { color: p.text }]}>{item.upvotes}</Text>
|
|
172
|
+
</TouchableOpacity>
|
|
173
|
+
|
|
174
|
+
<View style={styles.cardBody}>
|
|
175
|
+
<Text style={[styles.cardTitle, { color: p.text }]} numberOfLines={2}>
|
|
176
|
+
{displayTitle}
|
|
177
|
+
</Text>
|
|
178
|
+
{hasDescription && (
|
|
179
|
+
<Text style={[styles.cardDescription, { color: p.textFaint }]} numberOfLines={1}>
|
|
180
|
+
{item.content}
|
|
181
|
+
</Text>
|
|
182
|
+
)}
|
|
183
|
+
<View style={styles.cardMeta}>
|
|
184
|
+
<TypeBadge type={item.type} p={p} />
|
|
185
|
+
<StatusDot status={item.status} />
|
|
186
|
+
<Text style={[styles.cardDate, { color: p.textFaint }]}>
|
|
187
|
+
{formatDate(item.createdAt)}
|
|
188
|
+
</Text>
|
|
189
|
+
</View>
|
|
67
190
|
</View>
|
|
68
191
|
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
192
|
+
<Text style={[styles.chevron, { color: p.textFaint }]}>›</Text>
|
|
193
|
+
</TouchableOpacity>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── FeedDetail ───────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function FeedDetail({
|
|
200
|
+
p, accent, feedbackId, onBack,
|
|
201
|
+
}: {
|
|
202
|
+
p: Palette; accent: string; feedbackId: string; onBack: () => void;
|
|
203
|
+
}) {
|
|
204
|
+
const { vote, getItem } = useUpstep();
|
|
205
|
+
const [item, setItem] = useState<FeedbackWithComments | null>(null);
|
|
206
|
+
const [loading, setLoading] = useState(true);
|
|
207
|
+
const [voting, setVoting] = useState(false);
|
|
208
|
+
|
|
209
|
+
React.useEffect(() => {
|
|
210
|
+
let cancelled = false;
|
|
211
|
+
setLoading(true);
|
|
212
|
+
getItem(feedbackId).then((data) => {
|
|
213
|
+
if (!cancelled) { setItem(data); setLoading(false); }
|
|
214
|
+
}).catch(() => {
|
|
215
|
+
if (!cancelled) setLoading(false);
|
|
216
|
+
});
|
|
217
|
+
return () => { cancelled = true; };
|
|
218
|
+
}, [feedbackId, getItem]);
|
|
219
|
+
|
|
220
|
+
async function handleVote() {
|
|
221
|
+
if (!item || voting) return;
|
|
222
|
+
setVoting(true);
|
|
223
|
+
try {
|
|
224
|
+
await vote(item.id, "UP");
|
|
225
|
+
// Refresh
|
|
226
|
+
const updated = await getItem(feedbackId);
|
|
227
|
+
setItem(updated);
|
|
228
|
+
} finally {
|
|
229
|
+
setVoting(false);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<>
|
|
235
|
+
<BottomSheetView style={[styles.header, { borderBottomColor: p.border }]}>
|
|
236
|
+
<TouchableOpacity onPress={onBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}>
|
|
237
|
+
<Text style={[styles.backBtn, { color: accent }]}>← Back</Text>
|
|
238
|
+
</TouchableOpacity>
|
|
239
|
+
</BottomSheetView>
|
|
240
|
+
|
|
241
|
+
{loading || !item ? (
|
|
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} />
|
|
92
254
|
</View>
|
|
255
|
+
|
|
256
|
+
{/* Description */}
|
|
257
|
+
<Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
|
|
258
|
+
|
|
259
|
+
{/* Upvote */}
|
|
93
260
|
<TouchableOpacity
|
|
94
|
-
onPress={
|
|
95
|
-
disabled={
|
|
96
|
-
|
|
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
|
+
]}
|
|
97
271
|
>
|
|
98
|
-
{
|
|
99
|
-
<ActivityIndicator
|
|
272
|
+
{voting ? (
|
|
273
|
+
<ActivityIndicator size="small" color={accent} />
|
|
100
274
|
) : (
|
|
101
|
-
|
|
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
|
+
</>
|
|
102
282
|
)}
|
|
103
283
|
</TouchableOpacity>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
284
|
+
|
|
285
|
+
{/* Developer comments */}
|
|
286
|
+
{item.comments.length > 0 && (
|
|
287
|
+
<View style={[styles.commentsSection, { borderTopColor: p.border }]}>
|
|
288
|
+
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>
|
|
289
|
+
DEVELOPER RESPONSE
|
|
290
|
+
</Text>
|
|
291
|
+
{item.comments.map((c) => (
|
|
292
|
+
<CommentBubble key={c.id} comment={c} p={p} accent={accent} />
|
|
293
|
+
))}
|
|
294
|
+
</View>
|
|
114
295
|
)}
|
|
115
296
|
</BottomSheetScrollView>
|
|
116
297
|
)}
|
|
117
|
-
|
|
298
|
+
</>
|
|
118
299
|
);
|
|
119
300
|
}
|
|
120
301
|
|
|
121
|
-
function
|
|
302
|
+
function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; accent: string }) {
|
|
122
303
|
return (
|
|
123
|
-
<View style={styles.
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{item.type}
|
|
304
|
+
<View style={[styles.commentBubble, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
|
|
305
|
+
<View style={styles.commentMeta}>
|
|
306
|
+
<View style={[styles.ownerBadge, { backgroundColor: accent + "20" }]}>
|
|
307
|
+
<Text style={[styles.ownerBadgeText, { color: accent }]}>
|
|
308
|
+
{comment.authorName ?? "Team"}
|
|
129
309
|
</Text>
|
|
130
310
|
</View>
|
|
131
|
-
<Text style={styles.
|
|
311
|
+
<Text style={[styles.commentDate, { color: p.textFaint }]}>
|
|
312
|
+
{formatDate(comment.createdAt)}
|
|
313
|
+
</Text>
|
|
132
314
|
</View>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
315
|
+
<Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
|
|
316
|
+
</View>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── FeedCreate ───────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
const TYPES: FeedbackType[] = ["BUG", "FEATURE", "GENERAL"];
|
|
323
|
+
const TYPE_LABELS: Record<FeedbackType, string> = {
|
|
324
|
+
BUG: "Bug report",
|
|
325
|
+
FEATURE: "Feature request",
|
|
326
|
+
GENERAL: "General",
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
function FeedCreate({ p, accent, onBack }: { p: Palette; accent: string; onBack: () => void }) {
|
|
330
|
+
const { submit } = useUpstep();
|
|
331
|
+
const [title, setTitle] = useState("");
|
|
332
|
+
const [description, setDescription] = useState("");
|
|
333
|
+
const [type, setType] = useState<FeedbackType>("GENERAL");
|
|
334
|
+
const [loading, setLoading] = useState(false);
|
|
335
|
+
const [success, setSuccess] = useState(false);
|
|
336
|
+
const [error, setError] = useState("");
|
|
337
|
+
|
|
338
|
+
async function handleSubmit() {
|
|
339
|
+
if (!title.trim() || loading) return;
|
|
340
|
+
setLoading(true);
|
|
341
|
+
setError("");
|
|
342
|
+
try {
|
|
343
|
+
await submit(title.trim(), description.trim() || title.trim(), type);
|
|
344
|
+
setSuccess(true);
|
|
345
|
+
// Return to list after a moment
|
|
346
|
+
setTimeout(onBack, 1400);
|
|
347
|
+
} catch {
|
|
348
|
+
setError("Something went wrong. Please try again.");
|
|
349
|
+
} finally {
|
|
350
|
+
setLoading(false);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<>
|
|
356
|
+
<BottomSheetView style={[styles.header, { borderBottomColor: p.border }]}>
|
|
357
|
+
<TouchableOpacity onPress={onBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}>
|
|
358
|
+
<Text style={[styles.backBtn, { color: accent }]}>Cancel</Text>
|
|
139
359
|
</TouchableOpacity>
|
|
140
|
-
|
|
360
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
|
|
361
|
+
<View style={{ width: 60 }} />
|
|
362
|
+
</BottomSheetView>
|
|
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}
|
|
438
|
+
|
|
439
|
+
<TouchableOpacity
|
|
440
|
+
onPress={handleSubmit}
|
|
441
|
+
disabled={loading || !title.trim()}
|
|
442
|
+
activeOpacity={0.85}
|
|
443
|
+
style={[
|
|
444
|
+
styles.submitBtn,
|
|
445
|
+
{ backgroundColor: accent },
|
|
446
|
+
(loading || !title.trim()) && styles.submitBtnDisabled,
|
|
447
|
+
]}
|
|
448
|
+
>
|
|
449
|
+
{loading ? (
|
|
450
|
+
<ActivityIndicator color="#fff" />
|
|
451
|
+
) : (
|
|
452
|
+
<Text style={styles.submitBtnText}>Submit feedback</Text>
|
|
453
|
+
)}
|
|
454
|
+
</TouchableOpacity>
|
|
455
|
+
</>
|
|
456
|
+
)}
|
|
457
|
+
</BottomSheetScrollView>
|
|
458
|
+
</>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Shared sub-components ────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
function EmptyState({ p, message }: { p: Palette; message: string }) {
|
|
465
|
+
return (
|
|
466
|
+
<View style={styles.empty}>
|
|
467
|
+
<Text style={[styles.emptyText, { color: p.textFaint }]}>{message}</Text>
|
|
468
|
+
</View>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const TYPE_BG: Record<FeedbackType, string> = {
|
|
473
|
+
BUG: "#fdecec", FEATURE: "#e9f0fd", GENERAL: "",
|
|
474
|
+
};
|
|
475
|
+
const TYPE_FG: Record<FeedbackType, string> = {
|
|
476
|
+
BUG: "#d6453d", FEATURE: "#3b76d6", GENERAL: "",
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
|
|
480
|
+
return (
|
|
481
|
+
<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>
|
|
485
|
+
</View>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
490
|
+
OPEN: "#b45309",
|
|
491
|
+
IN_PROGRESS: "#1d4ed8",
|
|
492
|
+
DONE: "#15803d",
|
|
493
|
+
PENDING: "#ea580c",
|
|
494
|
+
CLOSED: "#6b7280",
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
function StatusDot({ status }: { status: string }) {
|
|
498
|
+
const color = STATUS_COLORS[status] ?? "#9ca3af";
|
|
499
|
+
return <View style={[styles.statusDot, { backgroundColor: color }]} />;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function StatusBadge({ status, p }: { status: string; p: Palette }) {
|
|
503
|
+
const label = status.replace("_", " ");
|
|
504
|
+
return (
|
|
505
|
+
<View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
|
|
506
|
+
<StatusDot status={status} />
|
|
507
|
+
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{label}</Text>
|
|
141
508
|
</View>
|
|
142
509
|
);
|
|
143
510
|
}
|
|
144
511
|
|
|
512
|
+
function formatDate(iso: string) {
|
|
513
|
+
const d = new Date(iso);
|
|
514
|
+
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
518
|
+
|
|
145
519
|
const styles = StyleSheet.create({
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
520
|
+
// Header
|
|
521
|
+
header: {
|
|
522
|
+
flexDirection: "row",
|
|
523
|
+
alignItems: "center",
|
|
524
|
+
justifyContent: "space-between",
|
|
525
|
+
paddingHorizontal: 20,
|
|
526
|
+
paddingVertical: 14,
|
|
527
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
528
|
+
},
|
|
529
|
+
headerTitle: { fontSize: 16, fontWeight: "700", letterSpacing: -0.3 },
|
|
530
|
+
backBtn: { fontSize: 14, fontWeight: "600" },
|
|
531
|
+
newBtn: {
|
|
532
|
+
borderRadius: 20,
|
|
533
|
+
paddingHorizontal: 14,
|
|
534
|
+
paddingVertical: 7,
|
|
535
|
+
},
|
|
536
|
+
newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
|
|
537
|
+
|
|
538
|
+
// List
|
|
539
|
+
listBody: { padding: 16, paddingBottom: 40 },
|
|
540
|
+
|
|
541
|
+
// Card
|
|
542
|
+
card: {
|
|
543
|
+
flexDirection: "row",
|
|
544
|
+
alignItems: "center",
|
|
545
|
+
gap: 12,
|
|
546
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
547
|
+
borderRadius: 16,
|
|
548
|
+
padding: 14,
|
|
549
|
+
marginBottom: 10,
|
|
550
|
+
},
|
|
551
|
+
voteBox: {
|
|
552
|
+
alignItems: "center",
|
|
553
|
+
justifyContent: "center",
|
|
554
|
+
width: 44,
|
|
555
|
+
paddingVertical: 8,
|
|
556
|
+
borderRadius: 11,
|
|
557
|
+
borderWidth: 1,
|
|
558
|
+
},
|
|
559
|
+
voteArrow: { fontSize: 11, lineHeight: 13 },
|
|
560
|
+
voteCount: { fontSize: 15, fontWeight: "700", lineHeight: 18 },
|
|
561
|
+
cardBody: { flex: 1 },
|
|
562
|
+
cardTitle: { fontSize: 14, fontWeight: "600", lineHeight: 19, marginBottom: 2 },
|
|
563
|
+
cardDescription: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
|
|
564
|
+
cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
565
|
+
cardDate: { fontSize: 11 },
|
|
566
|
+
chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
|
|
567
|
+
|
|
568
|
+
// Detail
|
|
569
|
+
detailBody: { padding: 20, paddingBottom: 50 },
|
|
570
|
+
detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
|
|
571
|
+
detailBadgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
|
|
572
|
+
detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
|
|
573
|
+
upvoteBtn: {
|
|
574
|
+
flexDirection: "row",
|
|
575
|
+
alignItems: "center",
|
|
576
|
+
gap: 8,
|
|
577
|
+
alignSelf: "flex-start",
|
|
578
|
+
borderWidth: 1,
|
|
579
|
+
borderRadius: 12,
|
|
580
|
+
paddingHorizontal: 16,
|
|
581
|
+
paddingVertical: 10,
|
|
582
|
+
marginBottom: 8,
|
|
583
|
+
minWidth: 80,
|
|
584
|
+
justifyContent: "center",
|
|
585
|
+
},
|
|
586
|
+
upvoteArrow: { fontSize: 13, lineHeight: 15 },
|
|
587
|
+
upvoteCount: { fontSize: 18, fontWeight: "700" },
|
|
588
|
+
upvoteLabel: { fontSize: 13, fontWeight: "500" },
|
|
589
|
+
|
|
590
|
+
// Comments
|
|
591
|
+
commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
|
|
592
|
+
commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
|
|
593
|
+
commentBubble: {
|
|
594
|
+
borderRadius: 12,
|
|
595
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
596
|
+
padding: 14,
|
|
597
|
+
marginBottom: 10,
|
|
598
|
+
},
|
|
599
|
+
commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
|
|
600
|
+
ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
|
|
601
|
+
ownerBadgeText: { fontSize: 11, fontWeight: "700" },
|
|
602
|
+
commentDate: { fontSize: 11 },
|
|
603
|
+
commentContent: { fontSize: 14, lineHeight: 21 },
|
|
604
|
+
|
|
605
|
+
// Create
|
|
606
|
+
createBody: { padding: 20, paddingBottom: 50 },
|
|
607
|
+
fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
|
|
608
|
+
typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
|
|
609
|
+
typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
|
|
610
|
+
typeBtnText: { fontSize: 13, fontWeight: "600" },
|
|
611
|
+
titleInput: {
|
|
612
|
+
borderWidth: 1, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12,
|
|
613
|
+
fontSize: 15, marginBottom: 20,
|
|
614
|
+
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
615
|
+
},
|
|
616
|
+
descInput: {
|
|
617
|
+
borderWidth: 1, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12,
|
|
618
|
+
fontSize: 14, minHeight: 110, marginBottom: 20,
|
|
619
|
+
...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
|
|
620
|
+
},
|
|
621
|
+
errorText: { fontSize: 13, marginBottom: 12, fontWeight: "500" },
|
|
622
|
+
submitBtn: { borderRadius: 12, padding: 14, alignItems: "center" },
|
|
623
|
+
submitBtnDisabled: { opacity: 0.5 },
|
|
624
|
+
submitBtnText: { color: "#fff", fontSize: 15, fontWeight: "700" },
|
|
625
|
+
successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
|
|
626
|
+
successSub: { fontSize: 14, textAlign: "center" },
|
|
627
|
+
|
|
628
|
+
// Shared
|
|
629
|
+
empty: { alignItems: "center", paddingVertical: 60 },
|
|
630
|
+
emptyText: { fontSize: 14 },
|
|
631
|
+
centered: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: 60 },
|
|
632
|
+
badge: { paddingHorizontal: 7, paddingVertical: 3, borderRadius: 6 },
|
|
633
|
+
badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
|
|
634
|
+
statusDot: { width: 6, height: 6, borderRadius: 3 },
|
|
635
|
+
statusBadge: {
|
|
636
|
+
flexDirection: "row", alignItems: "center", gap: 5, paddingHorizontal: 8,
|
|
637
|
+
paddingVertical: 3, borderRadius: 6, borderWidth: StyleSheet.hairlineWidth,
|
|
638
|
+
},
|
|
639
|
+
statusBadgeText: { fontSize: 10, fontWeight: "600", textTransform: "capitalize" },
|
|
179
640
|
});
|
package/src/api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Feedback,
|
|
3
3
|
FeedbackListResponse,
|
|
4
|
+
FeedbackWithComments,
|
|
4
5
|
SubmitFeedbackPayload,
|
|
5
6
|
UpstepConfig,
|
|
6
7
|
VoteValue,
|
|
@@ -31,11 +32,21 @@ export class UpstepApiClient {
|
|
|
31
32
|
if (params?.cursor) qs.set("cursor", params.cursor);
|
|
32
33
|
if (params?.limit) qs.set("limit", String(params.limit));
|
|
33
34
|
if (params?.sort) qs.set("sort", params.sort);
|
|
35
|
+
// Pass userId so the server can include the user's own pending items.
|
|
36
|
+
if (this.userId) qs.set("endUserId", this.userId);
|
|
34
37
|
const res = await fetch(`${this.baseUrl}/api/sdk/feedback?${qs}`, { headers: this.headers() });
|
|
35
38
|
if (!res.ok) throw new Error(`Upstep: ${res.status}`);
|
|
36
39
|
return res.json() as Promise<FeedbackListResponse>;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
async getItem(feedbackId: string): Promise<FeedbackWithComments> {
|
|
43
|
+
const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}`, {
|
|
44
|
+
headers: this.headers(),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) throw new Error(`Upstep: ${res.status}`);
|
|
47
|
+
return res.json() as Promise<FeedbackWithComments>;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
async submitFeedback(payload: SubmitFeedbackPayload): Promise<Feedback> {
|
|
40
51
|
const res = await fetch(`${this.baseUrl}/api/sdk/feedback`, {
|
|
41
52
|
method: "POST",
|