msaas-feedback 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ node_modules/
2
+ dist/
3
+ .next/
4
+ .turbo/
5
+ *.pyc
6
+ __pycache__/
7
+ .venv/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .env
12
+ .env.local
13
+ .env.*.local
14
+ .DS_Store
15
+ coverage/
16
+
17
+ # Runtime artifacts
18
+ logs_llm/
19
+ vectors.db
20
+ vectors.db-shm
21
+ vectors.db-wal
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: msaas-feedback
3
+ Version: 0.1.0
4
+ Summary: User feedback and feature request management for SaaS
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: msaas-api-core
7
+ Requires-Dist: msaas-errors
8
+ Requires-Dist: pydantic>=2.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: fastapi>=0.110.0; extra == 'dev'
11
+ Requires-Dist: httpx>=0.27; extra == 'dev'
12
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Requires-Dist: ruff>=0.8; extra == 'dev'
15
+ Provides-Extra: fastapi
16
+ Requires-Dist: fastapi>=0.110.0; extra == 'fastapi'
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@willianpinho/feedback",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "registry": "https://npm.pkg.github.com"
6
+ },
7
+ "description": "NPS survey and feedback widgets for React SaaS apps",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "vitest run"
22
+ },
23
+ "dependencies": {
24
+ "lucide-react": "^0.468.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19.0.0",
31
+ "typescript": "^5.7.0",
32
+ "vitest": "^3.0.0"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ]
37
+ }
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "msaas-feedback"
7
+ version = "0.1.0"
8
+ description = "User feedback and feature request management for SaaS"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "msaas-api-core",
12
+ "msaas-errors","pydantic>=2.0"
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ fastapi = ["fastapi>=0.110.0"]
17
+ dev = [
18
+ "pytest>=8.0",
19
+ "pytest-asyncio>=0.24",
20
+ "httpx>=0.27",
21
+ "fastapi>=0.110.0",
22
+ "ruff>=0.8",
23
+ ]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/feedback"]
27
+
28
+ [tool.ruff]
29
+ target-version = "py312"
30
+ line-length = 100
31
+
32
+ [tool.ruff.lint]
33
+ select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "TCH"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ asyncio_mode = "auto"
38
+
39
+ [tool.uv.sources]
40
+ msaas-api-core = { workspace = true }
41
+ msaas-errors = { workspace = true }
@@ -0,0 +1,277 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, useState } from "react";
4
+ import { MessageSquare, X } from "lucide-react";
5
+ import { useClickOutside } from "../hooks/useClickOutside";
6
+ import type { FeedbackWidgetConfig } from "../types";
7
+
8
+ interface RatingOption {
9
+ value: number;
10
+ label: string;
11
+ symbol: string;
12
+ }
13
+
14
+ const DEFAULT_RATINGS: RatingOption[] = [
15
+ { value: 3, label: "Happy", symbol: ":-)" },
16
+ { value: 2, label: "Neutral", symbol: ":-|" },
17
+ { value: 1, label: "Sad", symbol: ":-(" },
18
+ ];
19
+
20
+ function getCurrentPage(): string {
21
+ if (typeof window === "undefined") return "/";
22
+ return window.location.pathname;
23
+ }
24
+
25
+ /**
26
+ * Floating feedback button with an expandable panel.
27
+ *
28
+ * Collects a smiley-face rating and an optional comment, then calls
29
+ * `onSubmit` with the structured payload. The current page is either
30
+ * passed explicitly via the `page` prop or auto-detected from
31
+ * `window.location.pathname`.
32
+ */
33
+ export function FeedbackWidget(props: FeedbackWidgetConfig) {
34
+ const { onSubmit, page, ratings = DEFAULT_RATINGS } = props;
35
+
36
+ const [open, setOpen] = useState(false);
37
+ const [selectedRating, setSelectedRating] = useState<number | null>(null);
38
+ const [comment, setComment] = useState("");
39
+ const [submitting, setSubmitting] = useState(false);
40
+ const [submitted, setSubmitted] = useState(false);
41
+
42
+ const panelRef = useRef<HTMLDivElement>(null);
43
+
44
+ const handleClose = useCallback(() => {
45
+ setOpen(false);
46
+ }, []);
47
+
48
+ useClickOutside(panelRef, handleClose);
49
+
50
+ function reset() {
51
+ setSelectedRating(null);
52
+ setComment("");
53
+ setSubmitted(false);
54
+ }
55
+
56
+ function handleToggle() {
57
+ if (open) {
58
+ setOpen(false);
59
+ } else {
60
+ reset();
61
+ setOpen(true);
62
+ }
63
+ }
64
+
65
+ async function handleSubmit() {
66
+ if (selectedRating === null) return;
67
+
68
+ setSubmitting(true);
69
+ try {
70
+ await onSubmit({
71
+ rating: selectedRating,
72
+ comment: comment.trim(),
73
+ page: page ?? getCurrentPage(),
74
+ });
75
+ setSubmitted(true);
76
+ } catch {
77
+ // Keep the panel open so the user can retry.
78
+ } finally {
79
+ setSubmitting(false);
80
+ }
81
+ }
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ position: "fixed",
87
+ bottom: 24,
88
+ right: 24,
89
+ zIndex: 9998,
90
+ fontFamily: "system-ui, -apple-system, sans-serif",
91
+ }}
92
+ >
93
+ {/* --- Panel --- */}
94
+ {open && (
95
+ <div
96
+ ref={panelRef}
97
+ style={{
98
+ position: "absolute",
99
+ bottom: 56,
100
+ right: 0,
101
+ width: 320,
102
+ background: "#ffffff",
103
+ borderRadius: 12,
104
+ boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
105
+ padding: 20,
106
+ }}
107
+ >
108
+ {/* Close */}
109
+ <button
110
+ onClick={handleClose}
111
+ aria-label="Close feedback"
112
+ style={{
113
+ position: "absolute",
114
+ top: 8,
115
+ right: 8,
116
+ background: "none",
117
+ border: "none",
118
+ cursor: "pointer",
119
+ padding: 4,
120
+ color: "#94a3b8",
121
+ }}
122
+ >
123
+ <X size={16} />
124
+ </button>
125
+
126
+ {submitted ? (
127
+ /* --- Thank you state --- */
128
+ <div style={{ textAlign: "center", padding: "8px 0" }}>
129
+ <p
130
+ style={{
131
+ margin: "0 0 4px",
132
+ fontSize: 16,
133
+ fontWeight: 600,
134
+ color: "#1e293b",
135
+ }}
136
+ >
137
+ Thank you!
138
+ </p>
139
+ <p style={{ margin: 0, fontSize: 13, color: "#64748b" }}>
140
+ We appreciate your feedback.
141
+ </p>
142
+ </div>
143
+ ) : (
144
+ /* --- Form --- */
145
+ <div>
146
+ <p
147
+ style={{
148
+ margin: "0 0 14px",
149
+ fontSize: 15,
150
+ fontWeight: 500,
151
+ color: "#1e293b",
152
+ }}
153
+ >
154
+ How is your experience?
155
+ </p>
156
+
157
+ {/* Rating buttons */}
158
+ <div
159
+ style={{
160
+ display: "flex",
161
+ gap: 12,
162
+ justifyContent: "center",
163
+ marginBottom: 14,
164
+ }}
165
+ >
166
+ {ratings.map((r) => (
167
+ <button
168
+ key={r.value}
169
+ onClick={() => setSelectedRating(r.value)}
170
+ aria-label={r.label}
171
+ style={{
172
+ display: "flex",
173
+ flexDirection: "column",
174
+ alignItems: "center",
175
+ gap: 4,
176
+ padding: "8px 14px",
177
+ borderRadius: 10,
178
+ border:
179
+ selectedRating === r.value
180
+ ? "2px solid #3b82f6"
181
+ : "2px solid #e2e8f0",
182
+ background:
183
+ selectedRating === r.value ? "#eff6ff" : "#f8fafc",
184
+ cursor: "pointer",
185
+ transition: "all 0.15s",
186
+ }}
187
+ >
188
+ <span style={{ fontSize: 24 }}>{r.symbol}</span>
189
+ <span
190
+ style={{
191
+ fontSize: 11,
192
+ color:
193
+ selectedRating === r.value ? "#3b82f6" : "#64748b",
194
+ fontWeight: selectedRating === r.value ? 600 : 400,
195
+ }}
196
+ >
197
+ {r.label}
198
+ </span>
199
+ </button>
200
+ ))}
201
+ </div>
202
+
203
+ {/* Comment */}
204
+ <textarea
205
+ value={comment}
206
+ onChange={(e) => setComment(e.target.value)}
207
+ rows={3}
208
+ placeholder="Tell us more... (optional)"
209
+ style={{
210
+ width: "100%",
211
+ borderRadius: 8,
212
+ border: "1px solid #e2e8f0",
213
+ padding: "8px 12px",
214
+ fontSize: 13,
215
+ resize: "vertical",
216
+ fontFamily: "inherit",
217
+ boxSizing: "border-box",
218
+ }}
219
+ />
220
+
221
+ {/* Submit */}
222
+ <button
223
+ onClick={() => void handleSubmit()}
224
+ disabled={selectedRating === null || submitting}
225
+ style={{
226
+ marginTop: 12,
227
+ width: "100%",
228
+ padding: "10px 0",
229
+ borderRadius: 8,
230
+ border: "none",
231
+ background:
232
+ selectedRating === null ? "#cbd5e1" : "#3b82f6",
233
+ color: "#ffffff",
234
+ fontSize: 14,
235
+ fontWeight: 600,
236
+ cursor: selectedRating === null ? "not-allowed" : "pointer",
237
+ opacity: submitting ? 0.6 : 1,
238
+ transition: "background 0.15s",
239
+ }}
240
+ >
241
+ {submitting ? "Sending..." : "Send Feedback"}
242
+ </button>
243
+ </div>
244
+ )}
245
+ </div>
246
+ )}
247
+
248
+ {/* --- Floating trigger button --- */}
249
+ <button
250
+ onClick={handleToggle}
251
+ aria-label={open ? "Close feedback" : "Give feedback"}
252
+ style={{
253
+ width: 48,
254
+ height: 48,
255
+ borderRadius: "50%",
256
+ border: "none",
257
+ background: "#3b82f6",
258
+ color: "#ffffff",
259
+ cursor: "pointer",
260
+ display: "flex",
261
+ alignItems: "center",
262
+ justifyContent: "center",
263
+ boxShadow: "0 4px 14px rgba(59,130,246,0.4)",
264
+ transition: "transform 0.15s",
265
+ }}
266
+ onMouseEnter={(e) => {
267
+ e.currentTarget.style.transform = "scale(1.08)";
268
+ }}
269
+ onMouseLeave={(e) => {
270
+ e.currentTarget.style.transform = "scale(1)";
271
+ }}
272
+ >
273
+ <MessageSquare size={22} />
274
+ </button>
275
+ </div>
276
+ );
277
+ }