fi-edback 0.2.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.
@@ -0,0 +1,1088 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client.ts
22
+ var client_exports = {};
23
+ __export(client_exports, {
24
+ FeedbackRoot: () => FeedbackRoot
25
+ });
26
+ module.exports = __toCommonJS(client_exports);
27
+
28
+ // src/components/FeedbackRoot.tsx
29
+ var import_react5 = require("react");
30
+
31
+ // src/components/FeedbackLauncher.tsx
32
+ var import_react4 = require("react");
33
+
34
+ // src/lib/i18n.ts
35
+ var translations = {
36
+ en: {
37
+ feedbackButton: "Feedback",
38
+ cancelButton: "Cancel",
39
+ instructionText: "Click the Feedback button to leave a comment",
40
+ messagePlaceholder: "What would you like to tell us?",
41
+ messageLabel: "Message",
42
+ nameLabel: "Name (optional)",
43
+ emailLabel: "Email (optional)",
44
+ submitButton: "Send feedback",
45
+ submitting: "Sending...",
46
+ successMessage: "Thank you!",
47
+ successDescription: "Your feedback has been submitted.",
48
+ errorTitle: "Error",
49
+ clickToPlace: "Click anywhere to place a feedback pin",
50
+ feedbackSubmitted: "Feedback submitted",
51
+ deleteFeedback: "Delete feedback",
52
+ by: "by",
53
+ anonymous: "Anonymous",
54
+ reactions: "Reactions"
55
+ },
56
+ de: {
57
+ feedbackButton: "Feedback",
58
+ cancelButton: "Abbrechen",
59
+ instructionText: "Klicken Sie auf die Feedback-Schaltfl\xE4che, um einen Kommentar zu hinterlassen",
60
+ messagePlaceholder: "Was m\xF6chten Sie uns mitteilen?",
61
+ messageLabel: "Nachricht",
62
+ nameLabel: "Name (optional)",
63
+ emailLabel: "E-Mail (optional)",
64
+ submitButton: "Feedback senden",
65
+ submitting: "Wird gesendet...",
66
+ successMessage: "Vielen Dank!",
67
+ successDescription: "Ihr Feedback wurde \xFCbermittelt.",
68
+ errorTitle: "Fehler",
69
+ clickToPlace: "Klicken Sie \xFCberall, um einen Feedback-Pin zu platzieren",
70
+ feedbackSubmitted: "Feedback \xFCbermittelt",
71
+ deleteFeedback: "Feedback l\xF6schen",
72
+ by: "von",
73
+ anonymous: "Anonym",
74
+ reactions: "Reaktionen"
75
+ },
76
+ ga: {
77
+ feedbackButton: "Aiseolas",
78
+ cancelButton: "Cealaigh",
79
+ instructionText: "Clice\xE1il an cnaipe Aiseolas chun tr\xE1cht a fh\xE1g\xE1il",
80
+ messagePlaceholder: "Cad ba mhaith leat a r\xE1 linn?",
81
+ messageLabel: "Teachtaireacht",
82
+ nameLabel: "Ainm (roghnach)",
83
+ emailLabel: "R\xEDomhphost (roghnach)",
84
+ submitButton: "Seol aiseolas",
85
+ submitting: "\xC1 sheoladh...",
86
+ successMessage: "Go raibh maith agat!",
87
+ successDescription: "Seoladh d'aiseolas.",
88
+ errorTitle: "Earr\xE1id",
89
+ clickToPlace: "Clice\xE1il \xE1it ar bith chun bior\xE1in aiseolais a chur",
90
+ feedbackSubmitted: "Aiseolas seolta",
91
+ deleteFeedback: "Scrios aiseolas",
92
+ by: "le",
93
+ anonymous: "Gan ainm",
94
+ reactions: "Imoibrithe"
95
+ }
96
+ };
97
+ function getTranslations(lang) {
98
+ return translations[lang];
99
+ }
100
+
101
+ // src/components/FeedbackOverlay.tsx
102
+ var import_jsx_runtime = require("react/jsx-runtime");
103
+ function FeedbackOverlay({
104
+ language,
105
+ onPinPlaced
106
+ }) {
107
+ const t = getTranslations(language);
108
+ function handleClick(e) {
109
+ const x = e.clientX + window.scrollX;
110
+ const y = e.clientY + window.scrollY;
111
+ onPinPlaced(x, y);
112
+ }
113
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
114
+ "div",
115
+ {
116
+ onClick: handleClick,
117
+ role: "button",
118
+ "aria-label": t.clickToPlace,
119
+ tabIndex: 0,
120
+ onKeyDown: (e) => {
121
+ if (e.key === "Escape") e.currentTarget.blur();
122
+ },
123
+ style: {
124
+ position: "fixed",
125
+ inset: 0,
126
+ zIndex: 9997,
127
+ cursor: "crosshair",
128
+ backgroundColor: "rgba(0, 0, 0, 0.04)"
129
+ }
130
+ }
131
+ );
132
+ }
133
+
134
+ // src/components/FeedbackPinLayer.tsx
135
+ var import_react = require("react");
136
+ var import_jsx_runtime2 = require("react/jsx-runtime");
137
+ function FeedbackPinLayer({
138
+ pins,
139
+ onPinClick,
140
+ onPinMoved,
141
+ title = "Feedback submitted"
142
+ }) {
143
+ const [draggingId, setDraggingId] = (0, import_react.useState)(null);
144
+ const [dragOffset, setDragOffset] = (0, import_react.useState)({ x: 0, y: 0 });
145
+ const [tempPosition, setTempPosition] = (0, import_react.useState)(null);
146
+ const dragStartPos = (0, import_react.useRef)({ x: 0, y: 0 });
147
+ const hasMoved = (0, import_react.useRef)(false);
148
+ const longPressTimer = (0, import_react.useRef)(null);
149
+ (0, import_react.useEffect)(() => {
150
+ if (!draggingId) return;
151
+ const handleMove = (clientX, clientY) => {
152
+ hasMoved.current = true;
153
+ const x = clientX + window.scrollX - dragOffset.x;
154
+ const y = clientY + window.scrollY - dragOffset.y;
155
+ setTempPosition({ x, y });
156
+ };
157
+ const handleMouseMove = (e) => {
158
+ handleMove(e.clientX, e.clientY);
159
+ };
160
+ const handleTouchMove = (e) => {
161
+ e.preventDefault();
162
+ const touch = e.touches[0];
163
+ handleMove(touch.clientX, touch.clientY);
164
+ };
165
+ const handleEnd = () => {
166
+ if (draggingId && tempPosition && hasMoved.current) {
167
+ if (onPinMoved) {
168
+ onPinMoved(draggingId, tempPosition.x, tempPosition.y);
169
+ }
170
+ }
171
+ setDraggingId(null);
172
+ setTempPosition(null);
173
+ hasMoved.current = false;
174
+ };
175
+ document.addEventListener("mousemove", handleMouseMove);
176
+ document.addEventListener("mouseup", handleEnd);
177
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
178
+ document.addEventListener("touchend", handleEnd);
179
+ return () => {
180
+ document.removeEventListener("mousemove", handleMouseMove);
181
+ document.removeEventListener("mouseup", handleEnd);
182
+ document.removeEventListener("touchmove", handleTouchMove);
183
+ document.removeEventListener("touchend", handleEnd);
184
+ };
185
+ }, [draggingId, dragOffset, tempPosition, onPinMoved]);
186
+ if (pins.length === 0) return null;
187
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: pins.map((pin) => {
188
+ const isDragging = draggingId === pin.id;
189
+ const position = isDragging && tempPosition ? tempPosition : { x: pin.x, y: pin.y };
190
+ const handleStart = (clientX, clientY) => {
191
+ hasMoved.current = false;
192
+ const pinCenterX = pin.x;
193
+ const pinCenterY = pin.y;
194
+ const offsetX = clientX + window.scrollX - pinCenterX;
195
+ const offsetY = clientY + window.scrollY - pinCenterY;
196
+ setDragOffset({ x: offsetX, y: offsetY });
197
+ setDraggingId(pin.id);
198
+ dragStartPos.current = { x: clientX, y: clientY };
199
+ };
200
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
201
+ "div",
202
+ {
203
+ title,
204
+ onMouseDown: (e) => {
205
+ e.stopPropagation();
206
+ handleStart(e.clientX, e.clientY);
207
+ },
208
+ onTouchStart: (e) => {
209
+ e.stopPropagation();
210
+ const touch = e.touches[0];
211
+ handleStart(touch.clientX, touch.clientY);
212
+ },
213
+ onClick: (e) => {
214
+ if (!hasMoved.current && onPinClick) {
215
+ e.stopPropagation();
216
+ onPinClick(pin.id);
217
+ }
218
+ },
219
+ style: {
220
+ position: "absolute",
221
+ left: position.x - 10,
222
+ top: position.y - 22,
223
+ zIndex: isDragging ? 9998 : 9996,
224
+ width: "20px",
225
+ height: "20px",
226
+ borderRadius: "50% 50% 50% 0",
227
+ transform: isDragging ? "rotate(-45deg) scale(1.15)" : "rotate(-45deg)",
228
+ backgroundColor: "#18181b",
229
+ border: "2px solid #fff",
230
+ boxShadow: isDragging ? "0 8px 24px rgba(0,0,0,0.4)" : "0 2px 6px rgba(0,0,0,0.3)",
231
+ pointerEvents: "auto",
232
+ cursor: isDragging ? "grabbing" : "grab",
233
+ transition: isDragging ? "none" : "transform 0.15s ease, box-shadow 0.15s ease",
234
+ userSelect: "none"
235
+ },
236
+ onMouseEnter: (e) => {
237
+ if (!isDragging) {
238
+ e.currentTarget.style.transform = "rotate(-45deg) scale(1.1)";
239
+ }
240
+ },
241
+ onMouseLeave: (e) => {
242
+ if (!isDragging) {
243
+ e.currentTarget.style.transform = "rotate(-45deg) scale(1)";
244
+ }
245
+ }
246
+ },
247
+ pin.id
248
+ );
249
+ }) });
250
+ }
251
+
252
+ // src/components/FeedbackForm.tsx
253
+ var import_react2 = require("react");
254
+
255
+ // src/lib/config.ts
256
+ var API_PATH = "/api/fi-edback";
257
+ var SESSION_COOKIE_NAME = "fi_session";
258
+
259
+ // src/lib/session.ts
260
+ function generateId() {
261
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
262
+ return crypto.randomUUID();
263
+ }
264
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
265
+ }
266
+ function getOrCreateSessionId() {
267
+ if (typeof document === "undefined") return "";
268
+ const cookies = Object.fromEntries(
269
+ document.cookie.split("; ").filter(Boolean).map((c) => {
270
+ const idx = c.indexOf("=");
271
+ return [c.slice(0, idx), c.slice(idx + 1)];
272
+ })
273
+ );
274
+ const existing = cookies[SESSION_COOKIE_NAME];
275
+ if (existing) return existing;
276
+ const id = generateId();
277
+ const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toUTCString();
278
+ document.cookie = `${SESSION_COOKIE_NAME}=${id}; expires=${expires}; path=/; SameSite=Lax`;
279
+ return id;
280
+ }
281
+
282
+ // src/components/FeedbackForm.tsx
283
+ var import_jsx_runtime3 = require("react/jsx-runtime");
284
+ function FeedbackForm({
285
+ x,
286
+ y,
287
+ projectSlug,
288
+ apiPath,
289
+ language,
290
+ onSubmitted,
291
+ onCancelled
292
+ }) {
293
+ const [message, setMessage] = (0, import_react2.useState)("");
294
+ const [name, setName] = (0, import_react2.useState)("");
295
+ const [email, setEmail] = (0, import_react2.useState)("");
296
+ const [website, setWebsite] = (0, import_react2.useState)("");
297
+ const [status, setStatus] = (0, import_react2.useState)("idle");
298
+ const [errorText, setErrorText] = (0, import_react2.useState)("");
299
+ const messageRef = (0, import_react2.useRef)(null);
300
+ const t = getTranslations(language);
301
+ (0, import_react2.useEffect)(() => {
302
+ messageRef.current?.focus();
303
+ }, []);
304
+ async function handleSubmit(e) {
305
+ e.preventDefault();
306
+ if (status === "submitting") return;
307
+ setStatus("submitting");
308
+ setErrorText("");
309
+ const sessionId = getOrCreateSessionId();
310
+ const payload = {
311
+ projectSlug,
312
+ pageUrl: window.location.href,
313
+ x,
314
+ y,
315
+ message,
316
+ name: name || void 0,
317
+ email: email || void 0,
318
+ sessionId,
319
+ website
320
+ // honeypot
321
+ };
322
+ try {
323
+ const res = await fetch(apiPath, {
324
+ method: "POST",
325
+ headers: { "Content-Type": "application/json" },
326
+ body: JSON.stringify(payload)
327
+ });
328
+ if (res.status === 429) {
329
+ setStatus("error");
330
+ setErrorText(
331
+ "Too many submissions \u2014 please wait a moment before trying again."
332
+ );
333
+ return;
334
+ }
335
+ if (!res.ok) {
336
+ const data2 = await res.json().catch(() => ({}));
337
+ setStatus("error");
338
+ setErrorText(
339
+ data2.error ?? "Something went wrong. Please try again."
340
+ );
341
+ return;
342
+ }
343
+ const data = await res.json();
344
+ setStatus("success");
345
+ setTimeout(() => {
346
+ onSubmitted(data.feedback);
347
+ }, 900);
348
+ } catch {
349
+ setStatus("error");
350
+ setErrorText(
351
+ "Network error \u2014 please check your connection and try again."
352
+ );
353
+ }
354
+ }
355
+ const viewportX = x - window.scrollX;
356
+ const viewportY = y - window.scrollY;
357
+ const isMobile = window.innerWidth < 640;
358
+ const formWidth = isMobile ? Math.min(window.innerWidth - 24, 300) : 300;
359
+ const formHeight = 320;
360
+ const left = isMobile ? 12 : Math.min(viewportX + 14, window.innerWidth - formWidth - 12);
361
+ const top = isMobile ? Math.min(
362
+ window.innerHeight - formHeight - 12,
363
+ window.innerHeight / 2 - formHeight / 2
364
+ ) : Math.min(viewportY + 14, window.innerHeight - formHeight - 12);
365
+ const inputStyle = {
366
+ width: "100%",
367
+ border: "1px solid #e4e4e7",
368
+ borderRadius: "6px",
369
+ padding: "8px 10px",
370
+ fontSize: "13px",
371
+ boxSizing: "border-box",
372
+ outline: "none",
373
+ fontFamily: "system-ui, sans-serif",
374
+ color: "#18181b",
375
+ backgroundColor: "#fff"
376
+ };
377
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
378
+ "div",
379
+ {
380
+ role: "dialog",
381
+ "aria-label": "Leave feedback",
382
+ style: {
383
+ position: "fixed",
384
+ zIndex: 9999,
385
+ top,
386
+ left,
387
+ width: `${formWidth}px`,
388
+ backgroundColor: "#fff",
389
+ borderRadius: "12px",
390
+ boxShadow: "0 8px 32px rgba(0,0,0,0.16)",
391
+ padding: "16px",
392
+ fontFamily: "system-ui, sans-serif",
393
+ fontSize: "14px"
394
+ },
395
+ children: status === "success" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
396
+ "div",
397
+ {
398
+ style: { textAlign: "center", padding: "20px 0", color: "#16a34a" },
399
+ children: [
400
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "28px", marginBottom: "8px" }, children: "\u2713" }),
401
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontWeight: "600", color: "#18181b" }, children: t.successMessage }),
402
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#71717a", fontSize: "13px", marginTop: "4px" }, children: t.successDescription })
403
+ ]
404
+ }
405
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSubmit, children: [
406
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
407
+ "div",
408
+ {
409
+ style: {
410
+ marginBottom: "12px",
411
+ fontWeight: "600",
412
+ color: "#18181b",
413
+ fontSize: "14px"
414
+ },
415
+ children: t.feedbackButton
416
+ }
417
+ ),
418
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "none" }, "aria-hidden": "true", children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: "fi-website", children: "Website" }),
420
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
421
+ "input",
422
+ {
423
+ id: "fi-website",
424
+ type: "text",
425
+ value: website,
426
+ onChange: (e) => setWebsite(e.target.value),
427
+ tabIndex: -1,
428
+ autoComplete: "off"
429
+ }
430
+ )
431
+ ] }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: "10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
433
+ "textarea",
434
+ {
435
+ ref: messageRef,
436
+ value: message,
437
+ onChange: (e) => setMessage(e.target.value),
438
+ placeholder: t.messagePlaceholder,
439
+ required: true,
440
+ rows: 3,
441
+ style: { ...inputStyle, resize: "vertical" }
442
+ }
443
+ ) }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: "10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
445
+ "input",
446
+ {
447
+ type: "text",
448
+ value: name,
449
+ onChange: (e) => setName(e.target.value),
450
+ placeholder: t.nameLabel,
451
+ style: inputStyle
452
+ }
453
+ ) }),
454
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: "14px" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
455
+ "input",
456
+ {
457
+ type: "email",
458
+ value: email,
459
+ onChange: (e) => setEmail(e.target.value),
460
+ placeholder: t.emailLabel,
461
+ style: inputStyle
462
+ }
463
+ ) }),
464
+ errorText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
465
+ "div",
466
+ {
467
+ style: {
468
+ marginBottom: "10px",
469
+ color: "#ef4444",
470
+ fontSize: "12px",
471
+ lineHeight: "1.4"
472
+ },
473
+ children: errorText
474
+ }
475
+ ),
476
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
477
+ "div",
478
+ {
479
+ style: { display: "flex", gap: "8px", justifyContent: "flex-end" },
480
+ children: [
481
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
482
+ "button",
483
+ {
484
+ type: "button",
485
+ onClick: onCancelled,
486
+ style: {
487
+ padding: "7px 14px",
488
+ borderRadius: "6px",
489
+ border: "1px solid #e4e4e7",
490
+ backgroundColor: "transparent",
491
+ fontSize: "13px",
492
+ cursor: "pointer",
493
+ color: "#71717a",
494
+ fontFamily: "system-ui, sans-serif"
495
+ },
496
+ children: t.cancelButton
497
+ }
498
+ ),
499
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
500
+ "button",
501
+ {
502
+ type: "submit",
503
+ disabled: status === "submitting" || !message.trim(),
504
+ style: {
505
+ padding: "7px 14px",
506
+ borderRadius: "6px",
507
+ border: "none",
508
+ backgroundColor: "#18181b",
509
+ color: "#fff",
510
+ fontSize: "13px",
511
+ fontWeight: "500",
512
+ cursor: status === "submitting" || !message.trim() ? "not-allowed" : "pointer",
513
+ opacity: status === "submitting" || !message.trim() ? 0.5 : 1,
514
+ fontFamily: "system-ui, sans-serif"
515
+ },
516
+ children: status === "submitting" ? t.submitting : t.submitButton
517
+ }
518
+ )
519
+ ]
520
+ }
521
+ )
522
+ ] })
523
+ }
524
+ );
525
+ }
526
+
527
+ // src/components/FeedbackPopup.tsx
528
+ var import_react3 = require("react");
529
+ var import_jsx_runtime4 = require("react/jsx-runtime");
530
+ var REACTIONS = ["\u{1F44D}", "\u2705", "\u2764\uFE0F", "\u{1F525}", "\u{1F440}"];
531
+ function FeedbackPopup({
532
+ feedback,
533
+ apiPath,
534
+ language,
535
+ onDeleted,
536
+ onReactionToggled,
537
+ onClose
538
+ }) {
539
+ const [isDeleting, setIsDeleting] = (0, import_react3.useState)(false);
540
+ const [reactingTo, setReactingTo] = (0, import_react3.useState)(null);
541
+ const t = getTranslations(language);
542
+ async function handleReaction(reaction) {
543
+ if (reactingTo) return;
544
+ setReactingTo(reaction);
545
+ const sessionId = getOrCreateSessionId();
546
+ try {
547
+ const res = await fetch(apiPath, {
548
+ method: "PATCH",
549
+ headers: { "Content-Type": "application/json" },
550
+ body: JSON.stringify({
551
+ feedbackId: feedback.id,
552
+ reaction,
553
+ sessionId
554
+ })
555
+ });
556
+ if (res.ok) {
557
+ const data = await res.json();
558
+ onReactionToggled(feedback.id, reaction, data.added);
559
+ }
560
+ } catch (error) {
561
+ console.error("[fi-edback] Failed to toggle reaction:", error);
562
+ } finally {
563
+ setReactingTo(null);
564
+ }
565
+ }
566
+ async function handleDelete() {
567
+ if (isDeleting) return;
568
+ if (!confirm(t.deleteFeedback + "?")) return;
569
+ setIsDeleting(true);
570
+ try {
571
+ const res = await fetch(`${apiPath}?id=${feedback.id}`, {
572
+ method: "DELETE"
573
+ });
574
+ if (res.ok) {
575
+ onDeleted(feedback.id);
576
+ } else {
577
+ alert(t.errorTitle);
578
+ setIsDeleting(false);
579
+ }
580
+ } catch {
581
+ alert(t.errorTitle);
582
+ setIsDeleting(false);
583
+ }
584
+ }
585
+ const viewportX = feedback.x - window.scrollX;
586
+ const viewportY = feedback.y - window.scrollY;
587
+ const isMobile = window.innerWidth < 640;
588
+ const popupWidth = isMobile ? Math.min(window.innerWidth - 24, 300) : 300;
589
+ const popupMaxHeight = 400;
590
+ const left = isMobile ? 12 : Math.min(viewportX + 14, window.innerWidth - popupWidth - 12);
591
+ const top = isMobile ? Math.min(
592
+ window.innerHeight - popupMaxHeight - 12,
593
+ window.innerHeight / 2 - popupMaxHeight / 2
594
+ ) : Math.min(viewportY + 14, window.innerHeight - popupMaxHeight - 12);
595
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
596
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
597
+ "div",
598
+ {
599
+ onClick: onClose,
600
+ style: {
601
+ position: "fixed",
602
+ inset: 0,
603
+ zIndex: 9998,
604
+ backgroundColor: "transparent"
605
+ }
606
+ }
607
+ ),
608
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
609
+ "div",
610
+ {
611
+ role: "dialog",
612
+ "aria-label": "Feedback details",
613
+ style: {
614
+ position: "fixed",
615
+ zIndex: 9999,
616
+ top,
617
+ left,
618
+ width: `${popupWidth}px`,
619
+ maxHeight: `${popupMaxHeight}px`,
620
+ backgroundColor: "#fff",
621
+ borderRadius: "12px",
622
+ boxShadow: "0 8px 32px rgba(0,0,0,0.16)",
623
+ padding: "16px",
624
+ fontFamily: "system-ui, sans-serif",
625
+ fontSize: "14px",
626
+ overflow: "auto"
627
+ },
628
+ children: [
629
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
630
+ "button",
631
+ {
632
+ onClick: onClose,
633
+ "aria-label": "Close",
634
+ style: {
635
+ position: "absolute",
636
+ top: "12px",
637
+ right: "12px",
638
+ width: "24px",
639
+ height: "24px",
640
+ border: "none",
641
+ backgroundColor: "transparent",
642
+ cursor: "pointer",
643
+ fontSize: "18px",
644
+ lineHeight: "1",
645
+ color: "#71717a",
646
+ padding: 0
647
+ },
648
+ children: "\xD7"
649
+ }
650
+ ),
651
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
652
+ "div",
653
+ {
654
+ style: {
655
+ fontSize: "12px",
656
+ color: "#71717a",
657
+ marginBottom: "12px",
658
+ paddingRight: "24px"
659
+ },
660
+ children: [
661
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
662
+ t.by,
663
+ " ",
664
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: "#18181b" }, children: feedback.name || t.anonymous })
665
+ ] }),
666
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginTop: "4px" }, children: new Date(feedback.createdAt).toLocaleString(
667
+ language === "de" ? "de-DE" : "en-US",
668
+ {
669
+ dateStyle: "medium",
670
+ timeStyle: "short"
671
+ }
672
+ ) })
673
+ ]
674
+ }
675
+ ),
676
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
677
+ "div",
678
+ {
679
+ style: {
680
+ marginBottom: "16px",
681
+ lineHeight: "1.5",
682
+ color: "#18181b",
683
+ whiteSpace: "pre-wrap",
684
+ wordBreak: "break-word"
685
+ },
686
+ children: feedback.message
687
+ }
688
+ ),
689
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "16px" }, children: [
690
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
691
+ "div",
692
+ {
693
+ style: {
694
+ fontSize: "12px",
695
+ color: "#71717a",
696
+ marginBottom: "8px",
697
+ fontWeight: "500"
698
+ },
699
+ children: t.reactions
700
+ }
701
+ ),
702
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: REACTIONS.map((reaction) => {
703
+ const summary = feedback.reactions?.find(
704
+ (r) => r.reaction === reaction
705
+ );
706
+ const count = summary?.count || 0;
707
+ const hasReacted = summary?.hasReacted || false;
708
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
709
+ "button",
710
+ {
711
+ onClick: () => handleReaction(reaction),
712
+ disabled: reactingTo !== null,
713
+ style: {
714
+ display: "flex",
715
+ alignItems: "center",
716
+ gap: "4px",
717
+ padding: "6px 10px",
718
+ backgroundColor: hasReacted ? "#e0e7ff" : "#f4f4f5",
719
+ border: hasReacted ? "2px solid #6366f1" : "1px solid #e4e4e7",
720
+ borderRadius: "16px",
721
+ fontSize: "14px",
722
+ cursor: reactingTo ? "not-allowed" : "pointer",
723
+ opacity: reactingTo ? 0.6 : 1,
724
+ fontFamily: "system-ui, sans-serif",
725
+ transition: "all 0.15s ease"
726
+ },
727
+ onMouseEnter: (e) => {
728
+ if (!reactingTo) {
729
+ e.currentTarget.style.transform = "scale(1.05)";
730
+ }
731
+ },
732
+ onMouseLeave: (e) => {
733
+ e.currentTarget.style.transform = "scale(1)";
734
+ },
735
+ children: [
736
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: reaction }),
737
+ count > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
738
+ "span",
739
+ {
740
+ style: {
741
+ fontSize: "12px",
742
+ color: hasReacted ? "#6366f1" : "#71717a",
743
+ fontWeight: "600"
744
+ },
745
+ children: count
746
+ }
747
+ )
748
+ ]
749
+ },
750
+ reaction
751
+ );
752
+ }) })
753
+ ] }),
754
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
755
+ "button",
756
+ {
757
+ onClick: handleDelete,
758
+ disabled: isDeleting,
759
+ style: {
760
+ width: "100%",
761
+ backgroundColor: "#ef4444",
762
+ color: "#fff",
763
+ border: "none",
764
+ borderRadius: "6px",
765
+ padding: "8px 12px",
766
+ fontSize: "13px",
767
+ fontWeight: "500",
768
+ cursor: isDeleting ? "not-allowed" : "pointer",
769
+ opacity: isDeleting ? 0.6 : 1,
770
+ fontFamily: "system-ui, sans-serif"
771
+ },
772
+ children: isDeleting ? "..." : t.deleteFeedback
773
+ }
774
+ )
775
+ ]
776
+ }
777
+ )
778
+ ] });
779
+ }
780
+
781
+ // src/components/FeedbackLauncher.tsx
782
+ var import_jsx_runtime5 = require("react/jsx-runtime");
783
+ function FeedbackLauncher({ projectSlug }) {
784
+ const [language, setLanguage] = (0, import_react4.useState)("en");
785
+ const [isActive, setIsActive] = (0, import_react4.useState)(false);
786
+ const [pins, setPins] = (0, import_react4.useState)([]);
787
+ const [fullFeedback, setFullFeedback] = (0, import_react4.useState)([]);
788
+ const [pendingPin, setPendingPin] = (0, import_react4.useState)(
789
+ null
790
+ );
791
+ const [selectedFeedbackId, setSelectedFeedbackId] = (0, import_react4.useState)(
792
+ null
793
+ );
794
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(true);
795
+ const t = getTranslations(language);
796
+ (0, import_react4.useEffect)(() => {
797
+ async function fetchFeedback() {
798
+ try {
799
+ const sessionId = getOrCreateSessionId();
800
+ const params = new URLSearchParams({
801
+ projectSlug,
802
+ pageUrl: window.location.href,
803
+ sessionId
804
+ });
805
+ const res = await fetch(`${API_PATH}?${params}`);
806
+ if (res.ok) {
807
+ const data = await res.json();
808
+ setFullFeedback(data.feedback);
809
+ setPins(data.feedback.map((f) => ({ id: f.id, x: f.x, y: f.y })));
810
+ }
811
+ } catch (error) {
812
+ console.error("[fi-edback] Failed to fetch feedback:", error);
813
+ } finally {
814
+ setIsLoading(false);
815
+ }
816
+ }
817
+ fetchFeedback();
818
+ }, [projectSlug]);
819
+ function handleActivate() {
820
+ setIsActive(true);
821
+ setSelectedFeedbackId(null);
822
+ }
823
+ function handleDeactivate() {
824
+ setIsActive(false);
825
+ setPendingPin(null);
826
+ }
827
+ function handlePinPlaced(x, y) {
828
+ setIsActive(false);
829
+ setPendingPin({ x, y });
830
+ }
831
+ function handleFormSubmitted(feedback) {
832
+ setPins((prev) => [
833
+ ...prev,
834
+ { id: feedback.id, x: feedback.x, y: feedback.y }
835
+ ]);
836
+ setFullFeedback((prev) => [...prev, feedback]);
837
+ setPendingPin(null);
838
+ }
839
+ function handleFormCancelled() {
840
+ setPendingPin(null);
841
+ }
842
+ function handlePinClick(id) {
843
+ setSelectedFeedbackId(id);
844
+ }
845
+ function handlePopupClose() {
846
+ setSelectedFeedbackId(null);
847
+ }
848
+ function handleFeedbackDeleted(id) {
849
+ setPins((prev) => prev.filter((p) => p.id !== id));
850
+ setFullFeedback((prev) => prev.filter((f) => f.id !== id));
851
+ setSelectedFeedbackId(null);
852
+ }
853
+ function handleReactionToggled(feedbackId, reaction, added) {
854
+ setFullFeedback(
855
+ (prev) => prev.map((f) => {
856
+ if (f.id !== feedbackId) return f;
857
+ const reactions = f.reactions || [];
858
+ const existingIndex = reactions.findIndex(
859
+ (r) => r.reaction === reaction
860
+ );
861
+ if (existingIndex >= 0) {
862
+ const updated = [...reactions];
863
+ if (added) {
864
+ updated[existingIndex] = {
865
+ ...updated[existingIndex],
866
+ count: updated[existingIndex].count + 1,
867
+ hasReacted: true
868
+ };
869
+ } else {
870
+ const newCount = updated[existingIndex].count - 1;
871
+ if (newCount === 0) {
872
+ updated.splice(existingIndex, 1);
873
+ } else {
874
+ updated[existingIndex] = {
875
+ ...updated[existingIndex],
876
+ count: newCount,
877
+ hasReacted: false
878
+ };
879
+ }
880
+ }
881
+ return { ...f, reactions: updated };
882
+ } else {
883
+ return {
884
+ ...f,
885
+ reactions: [...reactions, { reaction, count: 1, hasReacted: true }]
886
+ };
887
+ }
888
+ })
889
+ );
890
+ }
891
+ async function handlePinMoved(id, x, y) {
892
+ setPins((prev) => prev.map((p) => p.id === id ? { ...p, x, y } : p));
893
+ setFullFeedback(
894
+ (prev) => prev.map((f) => f.id === id ? { ...f, x, y } : f)
895
+ );
896
+ try {
897
+ const res = await fetch(API_PATH, {
898
+ method: "PATCH",
899
+ headers: { "Content-Type": "application/json" },
900
+ body: JSON.stringify({ feedbackId: id, x, y })
901
+ });
902
+ if (!res.ok) {
903
+ console.error("[fi-edback] Failed to update pin position");
904
+ }
905
+ } catch (error) {
906
+ console.error("[fi-edback] Failed to update pin position:", error);
907
+ }
908
+ }
909
+ function toggleLanguage() {
910
+ setLanguage((prev) => {
911
+ if (prev === "en") return "de";
912
+ if (prev === "de") return "ga";
913
+ return "en";
914
+ });
915
+ }
916
+ const selectedFeedback = fullFeedback.find(
917
+ (f) => f.id === selectedFeedbackId
918
+ );
919
+ const isMobile = typeof window !== "undefined" && window.innerWidth < 640;
920
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
921
+ !isActive && !pendingPin && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
922
+ "div",
923
+ {
924
+ style: {
925
+ position: "fixed",
926
+ bottom: isMobile ? "90px" : "24px",
927
+ right: isMobile ? "12px" : "140px",
928
+ zIndex: 9998,
929
+ display: "flex",
930
+ gap: "4px",
931
+ backgroundColor: "#fff",
932
+ border: "1px solid #e4e4e7",
933
+ borderRadius: "6px",
934
+ padding: "4px",
935
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
936
+ fontFamily: "system-ui, sans-serif"
937
+ },
938
+ children: ["en", "de", "ga"].map((lang) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
939
+ "button",
940
+ {
941
+ onClick: () => setLanguage(lang),
942
+ "aria-label": `Switch to ${lang.toUpperCase()}`,
943
+ style: {
944
+ padding: isMobile ? "6px 10px" : "4px 8px",
945
+ fontSize: isMobile ? "13px" : "12px",
946
+ fontWeight: language === lang ? "700" : "500",
947
+ color: language === lang ? "#18181b" : "#71717a",
948
+ backgroundColor: language === lang ? "#f4f4f5" : "transparent",
949
+ border: "none",
950
+ borderRadius: "4px",
951
+ cursor: "pointer",
952
+ transition: "all 0.15s ease"
953
+ },
954
+ children: lang.toUpperCase()
955
+ },
956
+ lang
957
+ ))
958
+ }
959
+ ),
960
+ !isActive && !pendingPin && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
961
+ "div",
962
+ {
963
+ style: {
964
+ position: "fixed",
965
+ bottom: isMobile ? "12px" : "24px",
966
+ right: isMobile ? "12px" : "24px",
967
+ zIndex: 9998,
968
+ display: "flex",
969
+ flexDirection: "column",
970
+ alignItems: "flex-end",
971
+ gap: "8px"
972
+ },
973
+ children: [
974
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
975
+ "div",
976
+ {
977
+ style: {
978
+ fontSize: isMobile ? "11px" : "12px",
979
+ color: "#71717a",
980
+ fontFamily: "system-ui, sans-serif",
981
+ textAlign: "right",
982
+ maxWidth: isMobile ? "160px" : "200px",
983
+ lineHeight: "1.4"
984
+ },
985
+ children: t.instructionText
986
+ }
987
+ ),
988
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
989
+ "button",
990
+ {
991
+ onClick: handleActivate,
992
+ "aria-label": "Open feedback tool",
993
+ style: {
994
+ backgroundColor: "#18181b",
995
+ color: "#fff",
996
+ border: "none",
997
+ borderRadius: "9999px",
998
+ padding: isMobile ? "12px 20px" : "10px 18px",
999
+ fontSize: isMobile ? "15px" : "14px",
1000
+ fontWeight: "500",
1001
+ cursor: "pointer",
1002
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1003
+ fontFamily: "system-ui, sans-serif",
1004
+ letterSpacing: "0.01em"
1005
+ },
1006
+ children: t.feedbackButton
1007
+ }
1008
+ )
1009
+ ]
1010
+ }
1011
+ ),
1012
+ isActive && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1013
+ "button",
1014
+ {
1015
+ onClick: handleDeactivate,
1016
+ "aria-label": "Cancel feedback",
1017
+ style: {
1018
+ position: "fixed",
1019
+ bottom: isMobile ? "12px" : "24px",
1020
+ right: isMobile ? "12px" : "24px",
1021
+ zIndex: 9999,
1022
+ backgroundColor: "#ef4444",
1023
+ color: "#fff",
1024
+ border: "none",
1025
+ borderRadius: "9999px",
1026
+ padding: isMobile ? "12px 20px" : "10px 18px",
1027
+ fontSize: isMobile ? "15px" : "14px",
1028
+ fontWeight: "500",
1029
+ cursor: "pointer",
1030
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1031
+ fontFamily: "system-ui, sans-serif"
1032
+ },
1033
+ children: t.cancelButton
1034
+ }
1035
+ ),
1036
+ isActive && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(FeedbackOverlay, { language, onPinPlaced: handlePinPlaced }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1038
+ FeedbackPinLayer,
1039
+ {
1040
+ pins,
1041
+ onPinClick: handlePinClick,
1042
+ onPinMoved: handlePinMoved,
1043
+ title: t.feedbackSubmitted
1044
+ }
1045
+ ),
1046
+ pendingPin && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1047
+ FeedbackForm,
1048
+ {
1049
+ x: pendingPin.x,
1050
+ y: pendingPin.y,
1051
+ projectSlug,
1052
+ apiPath: API_PATH,
1053
+ language,
1054
+ onSubmitted: handleFormSubmitted,
1055
+ onCancelled: handleFormCancelled
1056
+ }
1057
+ ),
1058
+ selectedFeedback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1059
+ FeedbackPopup,
1060
+ {
1061
+ feedback: selectedFeedback,
1062
+ apiPath: API_PATH,
1063
+ language,
1064
+ onDeleted: handleFeedbackDeleted,
1065
+ onReactionToggled: handleReactionToggled,
1066
+ onClose: handlePopupClose
1067
+ }
1068
+ )
1069
+ ] });
1070
+ }
1071
+
1072
+ // src/components/FeedbackRoot.tsx
1073
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1074
+ function FeedbackRoot() {
1075
+ const [mounted, setMounted] = (0, import_react5.useState)(false);
1076
+ (0, import_react5.useEffect)(() => {
1077
+ setMounted(true);
1078
+ }, []);
1079
+ if (!mounted) return null;
1080
+ if (process.env.NEXT_PUBLIC_ENABLE_FEEDBACK !== "true") return null;
1081
+ const projectSlug = process.env.NEXT_PUBLIC_FEEDBACK_PROJECT_SLUG;
1082
+ if (!projectSlug) return null;
1083
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FeedbackLauncher, { projectSlug });
1084
+ }
1085
+ // Annotate the CommonJS export names for ESM import in node:
1086
+ 0 && (module.exports = {
1087
+ FeedbackRoot
1088
+ });