@pitangent/feedback-system 1.0.1 → 1.0.3
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/README.md +125 -98
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,136 +1,163 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @pitangent/feedback-system
|
|
2
2
|
|
|
3
|
-
A drop-in React feedback widget with
|
|
3
|
+
A drop-in React feedback widget with rating, bug report, and feature request
|
|
4
|
+
flows — including screenshot capture, screen recording, and rrweb session
|
|
4
5
|
recording/playback.
|
|
5
6
|
|
|
6
|
-
##
|
|
7
|
+
## Installation
|
|
7
8
|
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
FeedbackWidget, // the full feedback widget (trigger + panel + modal)
|
|
11
|
-
RRWebPlayerModal, // standalone session-recording preview player
|
|
12
|
-
SessionRecordingPreview // friendly alias for RRWebPlayerModal
|
|
13
|
-
} from '@wellnesspro/feedback';
|
|
14
|
-
import type { RRWebPlayerModalProps } from '@wellnesspro/feedback';
|
|
9
|
+
```bash
|
|
10
|
+
npm install @pitangent/feedback-system
|
|
15
11
|
```
|
|
16
12
|
|
|
17
13
|
### Peer dependencies
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
These are **not** bundled — your app must already have them installed:
|
|
20
16
|
|
|
21
17
|
- `react` `^18 || ^19`
|
|
22
18
|
- `react-dom` `^18 || ^19`
|
|
23
19
|
- `lucide-react` `^0.525.0`
|
|
24
20
|
- `sonner` `^2.0.0`
|
|
25
21
|
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Publishing to npm — step by step
|
|
29
|
-
|
|
30
|
-
> This is a **scoped** package (`@wellnesspro/...`). On the public npm registry,
|
|
31
|
-
> scoped packages are **private by default**, so you must pass `--access public`
|
|
32
|
-
> the first time (or set `publishConfig.access` — see below).
|
|
33
|
-
|
|
34
|
-
### One-time setup
|
|
35
|
-
|
|
36
|
-
1. **Create / verify the npm org.** The `@wellnesspro` scope must be an npm
|
|
37
|
-
organization (or user) you belong to. Create it at
|
|
38
|
-
<https://www.npmjs.com/org/create> (name: `wellnesspro`), or change the
|
|
39
|
-
package name in `package.json`.
|
|
40
|
-
|
|
41
|
-
2. **Recommended `package.json` additions** so publishes are safe and repeatable:
|
|
42
|
-
|
|
43
|
-
```jsonc
|
|
44
|
-
{
|
|
45
|
-
"files": ["dist"], // ship only the build output
|
|
46
|
-
"publishConfig": { "access": "public" },
|
|
47
|
-
"scripts": {
|
|
48
|
-
"prepublishOnly": "npm run build" // always build fresh dist before publish
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Every release
|
|
54
|
-
|
|
55
22
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# 1. Log in (once per machine)
|
|
59
|
-
npm login
|
|
60
|
-
npm whoami # confirm you're logged in
|
|
61
|
-
|
|
62
|
-
# 2. Bump the version (npm rejects re-publishing an existing version)
|
|
63
|
-
npm version patch # bug fix: 1.0.0 -> 1.0.1
|
|
64
|
-
# npm version minor # new feature: 1.0.0 -> 1.1.0
|
|
65
|
-
# npm version major # breaking: 1.0.0 -> 2.0.0
|
|
23
|
+
npm install react react-dom lucide-react sonner
|
|
24
|
+
```
|
|
66
25
|
|
|
67
|
-
|
|
68
|
-
|
|
26
|
+
> `sonner` powers the success/error toasts. Make sure you render its
|
|
27
|
+
> `<Toaster />` once near the root of your app, otherwise toasts won't appear.
|
|
69
28
|
|
|
70
|
-
|
|
71
|
-
npm publish --dry-run --access public
|
|
29
|
+
## Exports
|
|
72
30
|
|
|
73
|
-
|
|
74
|
-
|
|
31
|
+
```ts
|
|
32
|
+
import {
|
|
33
|
+
FeedbackWidget, // the full widget: trigger button + panel + modals
|
|
34
|
+
RRWebPlayerModal, // standalone session-recording preview player
|
|
35
|
+
SessionRecordingPreview, // friendly alias for RRWebPlayerModal
|
|
36
|
+
} from '@pitangent/feedback-system';
|
|
75
37
|
|
|
76
|
-
|
|
77
|
-
npm view @wellnesspro/feedback
|
|
38
|
+
import type { RRWebPlayerModalProps } from '@pitangent/feedback-system';
|
|
78
39
|
```
|
|
79
40
|
|
|
80
|
-
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
The recommended pattern is to create a small **wrapper component** that pulls
|
|
44
|
+
auth/tenant info from your app, then mount that wrapper once near the root of
|
|
45
|
+
your app (e.g. in your root layout). The widget renders a floating trigger
|
|
46
|
+
button and handles all panels/modals internally, and renders **nothing** when
|
|
47
|
+
`isAuthenticated` is `false`.
|
|
48
|
+
|
|
49
|
+
### 1. Create a wrapper component
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// FeedbackWidgetWrapper.tsx
|
|
53
|
+
'use client';
|
|
54
|
+
|
|
55
|
+
import { FeedbackWidget } from '@pitangent/feedback-system';
|
|
56
|
+
import { useAuth } from '@/shared/hooks';
|
|
57
|
+
import { AuthService, tenantService } from '@/infrastructure/auth';
|
|
58
|
+
|
|
59
|
+
export const FeedbackWidgetWrapper = () => {
|
|
60
|
+
const { isAuthenticated, user } = useAuth();
|
|
61
|
+
|
|
62
|
+
// Resolve a fresh token on each request
|
|
63
|
+
const getAuthToken = async () => {
|
|
64
|
+
return await AuthService.getValidAccessToken();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Resolve the current tenant (multi-tenant backends)
|
|
68
|
+
const getTenantId = () => {
|
|
69
|
+
return tenantService.getToken();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const feedbackApiUrl = process.env.NEXT_PUBLIC_FEEDBACK_API_URL;
|
|
73
|
+
const apiKey = process.env.NEXT_PUBLIC_FEEDBACK_API_KEY;
|
|
74
|
+
|
|
75
|
+
// Don't render until the widget is properly configured
|
|
76
|
+
if (!feedbackApiUrl || !apiKey) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<FeedbackWidget
|
|
82
|
+
isAuthenticated={isAuthenticated}
|
|
83
|
+
user={user} // { id, email_id } | null
|
|
84
|
+
apiUrl={feedbackApiUrl}
|
|
85
|
+
apiKey={apiKey}
|
|
86
|
+
getAuthToken={getAuthToken} // optional
|
|
87
|
+
getTenantId={getTenantId} // optional
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
```
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
### 2. Mount it once in your root layout
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// app/layout.tsx
|
|
97
|
+
import { FeedbackWidgetWrapper } from '@/shared/components/FeedbackWidgetWrapper';
|
|
98
|
+
import { Toaster } from 'sonner';
|
|
99
|
+
|
|
100
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
101
|
+
return (
|
|
102
|
+
<html lang="en">
|
|
103
|
+
<body>
|
|
104
|
+
{children}
|
|
105
|
+
|
|
106
|
+
{/* render once, near the root */}
|
|
107
|
+
<FeedbackWidgetWrapper />
|
|
108
|
+
<Toaster />
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
plan later **without renaming the package**.
|
|
115
|
+
### 3. Set the environment variables
|
|
86
116
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
```ini
|
|
118
|
+
NEXT_PUBLIC_FEEDBACK_API_URL=https://your-feedback-backend.example.com
|
|
119
|
+
NEXT_PUBLIC_FEEDBACK_API_KEY=your-api-key
|
|
120
|
+
```
|
|
91
121
|
|
|
92
|
-
|
|
122
|
+
## `<FeedbackWidget />` props
|
|
93
123
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
| Prop | Type | Required | Description |
|
|
125
|
+
| --- | --- | --- | --- |
|
|
126
|
+
| `isAuthenticated` | `boolean` | ✅ | When `false`, the widget renders nothing. |
|
|
127
|
+
| `user` | `{ id: string; email_id: string } \| null` | ✅ | Current user; `id` and `email_id` are sent with every submission. |
|
|
128
|
+
| `apiUrl` | `string` | ✅ | Base URL of the feedback backend. |
|
|
129
|
+
| `apiKey` | `string` | ✅ | API key used when fetching ticket priorities. |
|
|
130
|
+
| `getAuthToken` | `() => Promise<string \| null> \| string \| null` | — | Resolves a fresh auth token per request (sent as a bearer token). |
|
|
131
|
+
| `getTenantId` | `() => string \| null` | — | Resolves the tenant ID for multi-tenant backends. |
|
|
97
132
|
|
|
98
|
-
|
|
133
|
+
`getAuthToken` / `getTenantId` are called lazily on each request, so tokens are
|
|
134
|
+
always current — you don't need to re-render the widget when they change.
|
|
99
135
|
|
|
100
|
-
|
|
101
|
-
rename, no republish needed:
|
|
136
|
+
### What the widget submits
|
|
102
137
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
```
|
|
138
|
+
- **General feedback** — star rating + comment
|
|
139
|
+
- **Bug report** — title, description, priority, optional screenshot / recording / attachment
|
|
140
|
+
- **Feature request** — title, description, optional attachment
|
|
107
141
|
|
|
108
|
-
|
|
142
|
+
Submissions are sent as `multipart/form-data` to your `apiUrl`. Screenshots and
|
|
143
|
+
rrweb recordings are attached automatically when captured.
|
|
109
144
|
|
|
110
|
-
|
|
111
|
-
> downloadable, so making the package private only restricts **future** installs
|
|
112
|
-
> — it does not retract copies people already have. If the code must never be
|
|
113
|
-
> public, start on a paid/private plan from the first publish instead.
|
|
145
|
+
## Standalone session player
|
|
114
146
|
|
|
115
|
-
|
|
147
|
+
Use `RRWebPlayerModal` (aka `SessionRecordingPreview`) to replay a captured
|
|
148
|
+
rrweb event stream on its own, outside the widget:
|
|
116
149
|
|
|
117
|
-
|
|
150
|
+
```tsx
|
|
151
|
+
import { RRWebPlayerModal } from '@pitangent/feedback-system';
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
<RRWebPlayerModal
|
|
154
|
+
isOpen={open}
|
|
155
|
+
onClose={() => setOpen(false)}
|
|
156
|
+
events={recordingEvents}
|
|
157
|
+
/>;
|
|
121
158
|
```
|
|
122
159
|
|
|
123
160
|
---
|
|
124
161
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Your repo lives on GitLab (`gitlab.pitangent.net`). If you'd rather host this in
|
|
128
|
-
GitLab's package registry instead of npmjs.com, add a project-level `.npmrc`:
|
|
129
|
-
|
|
130
|
-
```ini
|
|
131
|
-
@wellnesspro:registry=https://gitlab.pitangent.net/api/v4/projects/<PROJECT_ID>/packages/npm/
|
|
132
|
-
//gitlab.pitangent.net/api/v4/projects/<PROJECT_ID>/packages/npm/:_authToken=${CI_JOB_TOKEN}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Then `npm publish` (no `--access` flag needed). This keeps the package private to
|
|
136
|
-
your GitLab group at no extra npm cost.
|
|
162
|
+
📦 Maintaining this package? See [PUBLISHING.md](./PUBLISHING.md) for how to
|
|
163
|
+
publish and ship updates to npm.
|
package/dist/index.js
CHANGED
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
transform: ${a} !important;
|
|
55
55
|
}
|
|
56
56
|
`)});let A=document.createElement("style");A.id="temp-fixed-style",A.innerHTML=V.join(`
|
|
57
|
-
`),document.head.appendChild(A);try{let s=document.documentElement;N=await(0,$e.toPng)(s,{width:window.innerWidth,height:window.innerHeight,canvasWidth:window.innerWidth,canvasHeight:window.innerHeight,pixelRatio:window.devicePixelRatio,filter:U=>{if(U instanceof HTMLElement||U&&"classList"in U){let z=U;if(z.classList&&z.classList.contains("feedback-no-capture"))return!1}return!0},style:{transform:`translate(-${u}px, -${D}px)`,transformOrigin:"top left",width:`${s.scrollWidth}px`,height:`${s.scrollHeight}px`}})}catch(s){console.warn("html2canvas direct capture failed, falling back to display media stream:",s);let U;try{U=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:"browser"},audio:!1,preferCurrentTab:!0,selfBrowserSurface:"include"})}catch(d){let a=d;if(a?.name==="NotAllowedError"||a?.name==="AbortError")throw a;U=await navigator.mediaDevices.getDisplayMedia({video:!0})}let z=document.createElement("video");z.srcObject=U,z.muted=!0,z.playsInline=!0,await new Promise(d=>{z.onloadedmetadata=()=>{z.play().then(()=>d()).catch(()=>d())},setTimeout(d,1e3)}),await new Promise(d=>setTimeout(d,150));let Z=document.createElement("canvas");Z.width=z.videoWidth||window.innerWidth,Z.height=z.videoHeight||window.innerHeight;let ie=Z.getContext("2d");ie&&(ie.drawImage(z,0,0,Z.width,Z.height),N=Z.toDataURL("image/png")),U.getTracks().forEach(d=>d.stop())}finally{A.remove(),W.forEach(s=>{s.removeAttribute("data-fixed-id")})}if(N)M(N),v(!0);else throw new Error("Could not generate screen capture data.")}catch(N){console.error("Failed to capture screenshot:",N);let W=N;if(W?.name==="NotAllowedError"||W?.name==="AbortError"){t(C.current||"general");return}be.toast.error("Capture Failed",{description:"Could not capture screenshot. Please try again."}),t(C.current||"general")}},F=async()=>{C.current=e,t(null),l(!1),Q(!0),O.current=[],await new Promise(N=>setTimeout(N,300));try{I.current=(0,He.record)({emit(N){O.current.push(N)},sampling:{mousemove:!0},blockClass:"feedback-no-capture",recordCanvas:!0}),console.log("[RRWeb Recorder] Recording started. Viewport:",{innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio})}catch(N){console.error("Failed to start rrweb recording:",N),be.toast.error("Failed to start session recording."),Q(!1),t(C.current||"general")}},X=()=>{if(I.current){try{I.current()}catch(N){console.error("Error stopping rrweb recording:",N)}I.current=void 0}if(Q(!1),O.current.length>0){let N=new Blob([JSON.stringify(O.current)],{type:"application/json"});b(N),m(URL.createObjectURL(N)),be.toast.success("Session recorded successfully!")}else be.toast.error("No events recorded during session.");t(C.current||"general")},S=()=>{r(null),b(null),H&&URL.revokeObjectURL(H),m(null),re(null),M(null),v(!1)};return{screenshotData:g,setScreenshotData:r,recordingBlob:y,setRecordingBlob:b,recordingUrl:H,setRecordingUrl:m,externalFile:w,setExternalFile:re,capturedImg:G,setCapturedImg:M,editorOpen:j,setEditorOpen:v,isRecording:$,prevActiveModal:C,recordingEvents:O.current,handleFileChange:R,captureScreenshot:ee,startRecording:F,stopRecording:X,clearAllMedia:S}};var Ce=Ke(require("axios")),Oe=async(e,t,l)=>{try{let g=l?.apiUrl,r="feedback";t==="bug"?r="bug-report":t==="feature"&&(r="feature-request");let y=`${g}/${r}`,b={};return l?.authToken&&(b.Authorization=`Bearer ${l.authToken}`),l?.tenantId&&(b["X-Tenant-ID"]=l.tenantId),l?.apiKey&&(b["x-api-key"]=l?.apiKey),(await Ce.default.post(y,e,{headers:b})).data}catch(g){throw console.error(`Error creating ${t} feedback:`,g),g}},Be=async e=>{try{let l=`${e?.apiUrl}/tickets/priorities`;return(await Ce.default.get(l)).data}catch(t){throw console.error("Error fetching ticket priorities:",t),t}};var pe=require("react/jsx-runtime"),at=(e,t)=>{let l=e.split(","),g=l[0].match(/:(.*?);/)?.[1]||"image/png",r=atob(l[1]),y=r.length,b=new Uint8Array(y);for(;y--;)b[y]=r.charCodeAt(y);return new File([b],t,{type:g})},Ue=({isAuthenticated:e,user:t,apiUrl:l,apiKey:g,getAuthToken:r,getTenantId:y})=>{let[b,H]=(0,ne.useState)(!1),[m,w]=(0,ne.useState)(null),[re,G]=(0,ne.useState)(!1),[M,j]=(0,ne.useState)(!1),[v,C]=(0,ne.useState)(0),[I,O]=(0,ne.useState)(0),[$,Q]=(0,ne.useState)(""),[R,ee]=(0,ne.useState)(""),[F,X]=(0,ne.useState)({}),[S,N]=(0,ne.useState)([]),[W,V]=(0,ne.useState)(""),u=(0,ne.useRef)(null),D=(0,ne.useRef)(null),A=(0,ne.useRef)(null),s=We({activeModal:m,setActiveModal:w,setPanelOpen:H});(0,ne.useEffect)(()=>{m==="bug"&&(async()=>{try{let f=r?await r():null,k=y?y():null,L=await Be({apiUrl:l,
|
|
57
|
+
`),document.head.appendChild(A);try{let s=document.documentElement;N=await(0,$e.toPng)(s,{width:window.innerWidth,height:window.innerHeight,canvasWidth:window.innerWidth,canvasHeight:window.innerHeight,pixelRatio:window.devicePixelRatio,filter:U=>{if(U instanceof HTMLElement||U&&"classList"in U){let z=U;if(z.classList&&z.classList.contains("feedback-no-capture"))return!1}return!0},style:{transform:`translate(-${u}px, -${D}px)`,transformOrigin:"top left",width:`${s.scrollWidth}px`,height:`${s.scrollHeight}px`}})}catch(s){console.warn("html2canvas direct capture failed, falling back to display media stream:",s);let U;try{U=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:"browser"},audio:!1,preferCurrentTab:!0,selfBrowserSurface:"include"})}catch(d){let a=d;if(a?.name==="NotAllowedError"||a?.name==="AbortError")throw a;U=await navigator.mediaDevices.getDisplayMedia({video:!0})}let z=document.createElement("video");z.srcObject=U,z.muted=!0,z.playsInline=!0,await new Promise(d=>{z.onloadedmetadata=()=>{z.play().then(()=>d()).catch(()=>d())},setTimeout(d,1e3)}),await new Promise(d=>setTimeout(d,150));let Z=document.createElement("canvas");Z.width=z.videoWidth||window.innerWidth,Z.height=z.videoHeight||window.innerHeight;let ie=Z.getContext("2d");ie&&(ie.drawImage(z,0,0,Z.width,Z.height),N=Z.toDataURL("image/png")),U.getTracks().forEach(d=>d.stop())}finally{A.remove(),W.forEach(s=>{s.removeAttribute("data-fixed-id")})}if(N)M(N),v(!0);else throw new Error("Could not generate screen capture data.")}catch(N){console.error("Failed to capture screenshot:",N);let W=N;if(W?.name==="NotAllowedError"||W?.name==="AbortError"){t(C.current||"general");return}be.toast.error("Capture Failed",{description:"Could not capture screenshot. Please try again."}),t(C.current||"general")}},F=async()=>{C.current=e,t(null),l(!1),Q(!0),O.current=[],await new Promise(N=>setTimeout(N,300));try{I.current=(0,He.record)({emit(N){O.current.push(N)},sampling:{mousemove:!0},blockClass:"feedback-no-capture",recordCanvas:!0}),console.log("[RRWeb Recorder] Recording started. Viewport:",{innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio})}catch(N){console.error("Failed to start rrweb recording:",N),be.toast.error("Failed to start session recording."),Q(!1),t(C.current||"general")}},X=()=>{if(I.current){try{I.current()}catch(N){console.error("Error stopping rrweb recording:",N)}I.current=void 0}if(Q(!1),O.current.length>0){let N=new Blob([JSON.stringify(O.current)],{type:"application/json"});b(N),m(URL.createObjectURL(N)),be.toast.success("Session recorded successfully!")}else be.toast.error("No events recorded during session.");t(C.current||"general")},S=()=>{r(null),b(null),H&&URL.revokeObjectURL(H),m(null),re(null),M(null),v(!1)};return{screenshotData:g,setScreenshotData:r,recordingBlob:y,setRecordingBlob:b,recordingUrl:H,setRecordingUrl:m,externalFile:w,setExternalFile:re,capturedImg:G,setCapturedImg:M,editorOpen:j,setEditorOpen:v,isRecording:$,prevActiveModal:C,recordingEvents:O.current,handleFileChange:R,captureScreenshot:ee,startRecording:F,stopRecording:X,clearAllMedia:S}};var Ce=Ke(require("axios")),Oe=async(e,t,l)=>{try{let g=l?.apiUrl,r="feedback";t==="bug"?r="bug-report":t==="feature"&&(r="feature-request");let y=`${g}/${r}`,b={};return l?.authToken&&(b.Authorization=`Bearer ${l.authToken}`),l?.tenantId&&(b["X-Tenant-ID"]=l.tenantId),l?.apiKey&&(b["x-api-key"]=l?.apiKey),(await Ce.default.post(y,e,{headers:b})).data}catch(g){throw console.error(`Error creating ${t} feedback:`,g),g}},Be=async e=>{try{let l=`${e?.apiUrl}/tickets/priorities`;return(await Ce.default.get(l)).data}catch(t){throw console.error("Error fetching ticket priorities:",t),t}};var pe=require("react/jsx-runtime"),at=(e,t)=>{let l=e.split(","),g=l[0].match(/:(.*?);/)?.[1]||"image/png",r=atob(l[1]),y=r.length,b=new Uint8Array(y);for(;y--;)b[y]=r.charCodeAt(y);return new File([b],t,{type:g})},Ue=({isAuthenticated:e,user:t,apiUrl:l,apiKey:g,getAuthToken:r,getTenantId:y})=>{let[b,H]=(0,ne.useState)(!1),[m,w]=(0,ne.useState)(null),[re,G]=(0,ne.useState)(!1),[M,j]=(0,ne.useState)(!1),[v,C]=(0,ne.useState)(0),[I,O]=(0,ne.useState)(0),[$,Q]=(0,ne.useState)(""),[R,ee]=(0,ne.useState)(""),[F,X]=(0,ne.useState)({}),[S,N]=(0,ne.useState)([]),[W,V]=(0,ne.useState)(""),u=(0,ne.useRef)(null),D=(0,ne.useRef)(null),A=(0,ne.useRef)(null),s=We({activeModal:m,setActiveModal:w,setPanelOpen:H});(0,ne.useEffect)(()=>{m==="bug"&&(async()=>{try{let f=r?await r():null,k=y?y():null,L=await Be({apiUrl:l,authToken:f,tenantId:k});if(L&&L.data){N(L.data);let o=L.data.find(i=>i.name.toLowerCase()==="low");o?V(o.id):L.data.length>0&&V(L.data[0].id)}}catch(f){console.error("Failed to fetch priorities:",f)}})()},[m,l,r,y]),(0,ne.useEffect)(()=>{let a=f=>{u.current&&!u.current.contains(f.target)&&D.current&&!D.current.contains(f.target)&&H(!1)};return b&&document.addEventListener("mousedown",a),()=>{document.removeEventListener("mousedown",a)}},[b]);let U=a=>{w(a),H(!1),C(0),O(0),Q(""),ee(""),V(""),s.clearAllMedia(),A.current&&(A.current.value=""),X({})},z=()=>{w(null),C(0),O(0),Q(""),ee(""),V(""),s.clearAllMedia(),A.current&&(A.current.value=""),X({})},Z=()=>{let a={};return m==="general"?(v===0&&(a.rating="Please select a rating"),R.trim()?R.trim().length<5&&(a.description="Please enter at least 5 characters"):a.description="Please enter your feedback"):(m==="bug"||m==="feature")&&($.trim()?$.length>100&&(a.title="Title must be 100 characters or less"):a.title="Please enter a title",R.trim()?R.trim().length<10&&(a.description="Please enter at least 10 characters"):a.description="Please enter details",m==="bug"&&!W&&(a.priority="Please select a priority")),X(a),Object.keys(a).length===0},ie=async a=>{if(a.preventDefault(),!(!Z()||!m)){G(!0);try{let f=r?await r():null,k=y?y():null,L=new FormData;if(L.append("user_id",String(t?.id??"")),L.append("user_email",t?.email_id||""),m==="general"&&v>0&&L.append("ratting",String(v)),$&&L.append("title",$),R&&L.append("description",R),m==="bug"&&W&&L.append("ticketPriorityId",W),s.screenshotData)try{let c=at(s.screenshotData,"screenshot.png");L.append("screenshot",c)}catch(c){console.error("Failed to convert screenshot to file",c)}s.recordingEvents&&s.recordingEvents.length>0&&L.append("events",JSON.stringify(s.recordingEvents)),s.externalFile&&L.append("attachement",s.externalFile);let o=await Oe(L,m,{apiUrl:l,apiKey:g,authToken:f,tenantId:k});console.log("res",o);let i="Thank you for your feedback!";m==="bug"?i="Thank you! The bug report has been submitted.":m==="feature"&&(i="Thank you! The feature request has been submitted."),ke.toast.success("Success",{description:i}),z()}catch(f){console.error("API submission error:",f),ke.toast.error("Submission Failed",{description:"An error occurred while submitting your feedback. Please try again."})}finally{G(!1)}}},d=a=>{F[a]&&X(f=>({...f,[a]:void 0}))};return e?(0,pe.jsxs)(pe.Fragment,{children:[(0,pe.jsx)(Ee,{buttonRef:D,onClick:()=>H(!b)}),(0,pe.jsx)(Me,{panelRef:u,isOpen:b,onClose:()=>H(!1),onOpenModal:U}),(0,pe.jsx)(Pe,{activeModal:m,onClose:z,onSubmit:ie,userEmail:t?.email_id||"",submitting:re,rating:v,setRating:C,hoverRating:I,setHoverRating:O,title:$,setTitle:Q,description:R,setDescription:ee,screenshotData:s.screenshotData,setScreenshotData:s.setScreenshotData,recordingBlob:s.recordingBlob,onRemoveRecording:()=>{s.setRecordingBlob(null),s.setRecordingUrl(null)},onPreviewRecording:()=>setTimeout(()=>j(!0),100),externalFile:s.externalFile,setExternalFile:s.setExternalFile,fileInputRef:A,handleFileChange:s.handleFileChange,onCaptureScreenshot:s.captureScreenshot,onStartRecording:s.startRecording,errors:F,onClearError:d,priorities:S,selectedPriorityId:W,setSelectedPriorityId:V}),(0,pe.jsx)(Ae,{isRecording:s.isRecording,onStopRecording:s.stopRecording}),(0,pe.jsx)(Le,{isOpen:s.editorOpen,capturedImg:s.capturedImg||"",onClose:()=>{s.setEditorOpen(!1),w(s.prevActiveModal.current||"general")},onSave:a=>{s.setScreenshotData(a),s.setEditorOpen(!1),w(s.prevActiveModal.current||"general"),ke.toast.success("Screenshot attached successfully!")}}),(0,pe.jsx)(ve,{isOpen:M,onClose:()=>j(!1),events:s.recordingEvents})]}):null};0&&(module.exports={FeedbackWidget,RRWebPlayerModal,SessionRecordingPreview});
|
package/dist/index.mjs
CHANGED
|
@@ -54,4 +54,4 @@ import{useEffect as Ze,useRef as Fe,useState as ie}from"react";import{MessageSqu
|
|
|
54
54
|
transform: ${r} !important;
|
|
55
55
|
}
|
|
56
56
|
`)});let F=document.createElement("style");F.id="temp-fixed-style",F.innerHTML=Y.join(`
|
|
57
|
-
`),document.head.appendChild(F);try{let o=document.documentElement;N=await jt(o,{width:window.innerWidth,height:window.innerHeight,canvasWidth:window.innerWidth,canvasHeight:window.innerHeight,pixelRatio:window.devicePixelRatio,filter:B=>{if(B instanceof HTMLElement||B&&"classList"in B){let z=B;if(z.classList&&z.classList.contains("feedback-no-capture"))return!1}return!0},style:{transform:`translate(-${d}px, -${E}px)`,transformOrigin:"top left",width:`${o.scrollWidth}px`,height:`${o.scrollHeight}px`}})}catch(o){console.warn("html2canvas direct capture failed, falling back to display media stream:",o);let B;try{B=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:"browser"},audio:!1,preferCurrentTab:!0,selfBrowserSurface:"include"})}catch(l){let r=l;if(r?.name==="NotAllowedError"||r?.name==="AbortError")throw r;B=await navigator.mediaDevices.getDisplayMedia({video:!0})}let z=document.createElement("video");z.srcObject=B,z.muted=!0,z.playsInline=!0,await new Promise(l=>{z.onloadedmetadata=()=>{z.play().then(()=>l()).catch(()=>l())},setTimeout(l,1e3)}),await new Promise(l=>setTimeout(l,150));let V=document.createElement("canvas");V.width=z.videoWidth||window.innerWidth,V.height=z.videoHeight||window.innerHeight;let ne=V.getContext("2d");ne&&(ne.drawImage(z,0,0,V.width,V.height),N=V.toDataURL("image/png")),B.getTracks().forEach(l=>l.stop())}finally{F.remove(),H.forEach(o=>{o.removeAttribute("data-fixed-id")})}if(N)P(N),y(!0);else throw new Error("Could not generate screen capture data.")}catch(N){console.error("Failed to capture screenshot:",N);let H=N;if(H?.name==="NotAllowedError"||H?.name==="AbortError"){t(C.current||"general");return}be.error("Capture Failed",{description:"Could not capture screenshot. Please try again."}),t(C.current||"general")}},T=async()=>{C.current=e,t(null),c(!1),K(!0),W.current=[],await new Promise(N=>setTimeout(N,300));try{D.current=zt({emit(N){W.current.push(N)},sampling:{mousemove:!0},blockClass:"feedback-no-capture",recordCanvas:!0}),console.log("[RRWeb Recorder] Recording started. Viewport:",{innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio})}catch(N){console.error("Failed to start rrweb recording:",N),be.error("Failed to start session recording."),K(!1),t(C.current||"general")}},X=()=>{if(D.current){try{D.current()}catch(N){console.error("Error stopping rrweb recording:",N)}D.current=void 0}if(K(!1),W.current.length>0){let N=new Blob([JSON.stringify(W.current)],{type:"application/json"});b(N),m(URL.createObjectURL(N)),be.success("Session recorded successfully!")}else be.error("No events recorded during session.");t(C.current||"general")},S=()=>{n(null),b(null),$&&URL.revokeObjectURL($),m(null),G(null),P(null),y(!1)};return{screenshotData:g,setScreenshotData:n,recordingBlob:x,setRecordingBlob:b,recordingUrl:$,setRecordingUrl:m,externalFile:v,setExternalFile:G,capturedImg:q,setCapturedImg:P,editorOpen:j,setEditorOpen:y,isRecording:L,prevActiveModal:C,recordingEvents:W.current,handleFileChange:R,captureScreenshot:J,startRecording:T,stopRecording:X,clearAllMedia:S}};import Je from"axios";var Ge=async(e,t,c)=>{try{let g=c?.apiUrl,n="feedback";t==="bug"?n="bug-report":t==="feature"&&(n="feature-request");let x=`${g}/${n}`,b={};return c?.authToken&&(b.Authorization=`Bearer ${c.authToken}`),c?.tenantId&&(b["X-Tenant-ID"]=c.tenantId),c?.apiKey&&(b["x-api-key"]=c?.apiKey),(await Je.post(x,e,{headers:b})).data}catch(g){throw console.error(`Error creating ${t} feedback:`,g),g}},Qe=async e=>{try{let c=`${e?.apiUrl}/tickets/priorities`;return(await Je.get(c)).data}catch(t){throw console.error("Error fetching ticket priorities:",t),t}};import{Fragment as _t,jsx as xe,jsxs as qt}from"react/jsx-runtime";var Xt=(e,t)=>{let c=e.split(","),g=c[0].match(/:(.*?);/)?.[1]||"image/png",n=atob(c[1]),x=n.length,b=new Uint8Array(x);for(;x--;)b[x]=n.charCodeAt(x);return new File([b],t,{type:g})},Yt=({isAuthenticated:e,user:t,apiUrl:c,apiKey:g,getAuthToken:n,getTenantId:x})=>{let[b,$]=ie(!1),[m,v]=ie(null),[G,q]=ie(!1),[P,j]=ie(!1),[y,C]=ie(0),[D,W]=ie(0),[L,K]=ie(""),[R,J]=ie(""),[T,X]=ie({}),[S,N]=ie([]),[H,Y]=ie(""),d=Fe(null),E=Fe(null),F=Fe(null),o=Ve({activeModal:m,setActiveModal:v,setPanelOpen:$});Ze(()=>{m==="bug"&&(async()=>{try{let f=n?await n():null,w=x?x():null,A=await Qe({apiUrl:c,
|
|
57
|
+
`),document.head.appendChild(F);try{let o=document.documentElement;N=await jt(o,{width:window.innerWidth,height:window.innerHeight,canvasWidth:window.innerWidth,canvasHeight:window.innerHeight,pixelRatio:window.devicePixelRatio,filter:B=>{if(B instanceof HTMLElement||B&&"classList"in B){let z=B;if(z.classList&&z.classList.contains("feedback-no-capture"))return!1}return!0},style:{transform:`translate(-${d}px, -${E}px)`,transformOrigin:"top left",width:`${o.scrollWidth}px`,height:`${o.scrollHeight}px`}})}catch(o){console.warn("html2canvas direct capture failed, falling back to display media stream:",o);let B;try{B=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:"browser"},audio:!1,preferCurrentTab:!0,selfBrowserSurface:"include"})}catch(l){let r=l;if(r?.name==="NotAllowedError"||r?.name==="AbortError")throw r;B=await navigator.mediaDevices.getDisplayMedia({video:!0})}let z=document.createElement("video");z.srcObject=B,z.muted=!0,z.playsInline=!0,await new Promise(l=>{z.onloadedmetadata=()=>{z.play().then(()=>l()).catch(()=>l())},setTimeout(l,1e3)}),await new Promise(l=>setTimeout(l,150));let V=document.createElement("canvas");V.width=z.videoWidth||window.innerWidth,V.height=z.videoHeight||window.innerHeight;let ne=V.getContext("2d");ne&&(ne.drawImage(z,0,0,V.width,V.height),N=V.toDataURL("image/png")),B.getTracks().forEach(l=>l.stop())}finally{F.remove(),H.forEach(o=>{o.removeAttribute("data-fixed-id")})}if(N)P(N),y(!0);else throw new Error("Could not generate screen capture data.")}catch(N){console.error("Failed to capture screenshot:",N);let H=N;if(H?.name==="NotAllowedError"||H?.name==="AbortError"){t(C.current||"general");return}be.error("Capture Failed",{description:"Could not capture screenshot. Please try again."}),t(C.current||"general")}},T=async()=>{C.current=e,t(null),c(!1),K(!0),W.current=[],await new Promise(N=>setTimeout(N,300));try{D.current=zt({emit(N){W.current.push(N)},sampling:{mousemove:!0},blockClass:"feedback-no-capture",recordCanvas:!0}),console.log("[RRWeb Recorder] Recording started. Viewport:",{innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio})}catch(N){console.error("Failed to start rrweb recording:",N),be.error("Failed to start session recording."),K(!1),t(C.current||"general")}},X=()=>{if(D.current){try{D.current()}catch(N){console.error("Error stopping rrweb recording:",N)}D.current=void 0}if(K(!1),W.current.length>0){let N=new Blob([JSON.stringify(W.current)],{type:"application/json"});b(N),m(URL.createObjectURL(N)),be.success("Session recorded successfully!")}else be.error("No events recorded during session.");t(C.current||"general")},S=()=>{n(null),b(null),$&&URL.revokeObjectURL($),m(null),G(null),P(null),y(!1)};return{screenshotData:g,setScreenshotData:n,recordingBlob:x,setRecordingBlob:b,recordingUrl:$,setRecordingUrl:m,externalFile:v,setExternalFile:G,capturedImg:q,setCapturedImg:P,editorOpen:j,setEditorOpen:y,isRecording:L,prevActiveModal:C,recordingEvents:W.current,handleFileChange:R,captureScreenshot:J,startRecording:T,stopRecording:X,clearAllMedia:S}};import Je from"axios";var Ge=async(e,t,c)=>{try{let g=c?.apiUrl,n="feedback";t==="bug"?n="bug-report":t==="feature"&&(n="feature-request");let x=`${g}/${n}`,b={};return c?.authToken&&(b.Authorization=`Bearer ${c.authToken}`),c?.tenantId&&(b["X-Tenant-ID"]=c.tenantId),c?.apiKey&&(b["x-api-key"]=c?.apiKey),(await Je.post(x,e,{headers:b})).data}catch(g){throw console.error(`Error creating ${t} feedback:`,g),g}},Qe=async e=>{try{let c=`${e?.apiUrl}/tickets/priorities`;return(await Je.get(c)).data}catch(t){throw console.error("Error fetching ticket priorities:",t),t}};import{Fragment as _t,jsx as xe,jsxs as qt}from"react/jsx-runtime";var Xt=(e,t)=>{let c=e.split(","),g=c[0].match(/:(.*?);/)?.[1]||"image/png",n=atob(c[1]),x=n.length,b=new Uint8Array(x);for(;x--;)b[x]=n.charCodeAt(x);return new File([b],t,{type:g})},Yt=({isAuthenticated:e,user:t,apiUrl:c,apiKey:g,getAuthToken:n,getTenantId:x})=>{let[b,$]=ie(!1),[m,v]=ie(null),[G,q]=ie(!1),[P,j]=ie(!1),[y,C]=ie(0),[D,W]=ie(0),[L,K]=ie(""),[R,J]=ie(""),[T,X]=ie({}),[S,N]=ie([]),[H,Y]=ie(""),d=Fe(null),E=Fe(null),F=Fe(null),o=Ve({activeModal:m,setActiveModal:v,setPanelOpen:$});Ze(()=>{m==="bug"&&(async()=>{try{let f=n?await n():null,w=x?x():null,A=await Qe({apiUrl:c,authToken:f,tenantId:w});if(A&&A.data){N(A.data);let a=A.data.find(s=>s.name.toLowerCase()==="low");a?Y(a.id):A.data.length>0&&Y(A.data[0].id)}}catch(f){console.error("Failed to fetch priorities:",f)}})()},[m,c,n,x]),Ze(()=>{let r=f=>{d.current&&!d.current.contains(f.target)&&E.current&&!E.current.contains(f.target)&&$(!1)};return b&&document.addEventListener("mousedown",r),()=>{document.removeEventListener("mousedown",r)}},[b]);let B=r=>{v(r),$(!1),C(0),W(0),K(""),J(""),Y(""),o.clearAllMedia(),F.current&&(F.current.value=""),X({})},z=()=>{v(null),C(0),W(0),K(""),J(""),Y(""),o.clearAllMedia(),F.current&&(F.current.value=""),X({})},V=()=>{let r={};return m==="general"?(y===0&&(r.rating="Please select a rating"),R.trim()?R.trim().length<5&&(r.description="Please enter at least 5 characters"):r.description="Please enter your feedback"):(m==="bug"||m==="feature")&&(L.trim()?L.length>100&&(r.title="Title must be 100 characters or less"):r.title="Please enter a title",R.trim()?R.trim().length<10&&(r.description="Please enter at least 10 characters"):r.description="Please enter details",m==="bug"&&!H&&(r.priority="Please select a priority")),X(r),Object.keys(r).length===0},ne=async r=>{if(r.preventDefault(),!(!V()||!m)){q(!0);try{let f=n?await n():null,w=x?x():null,A=new FormData;if(A.append("user_id",String(t?.id??"")),A.append("user_email",t?.email_id||""),m==="general"&&y>0&&A.append("ratting",String(y)),L&&A.append("title",L),R&&A.append("description",R),m==="bug"&&H&&A.append("ticketPriorityId",H),o.screenshotData)try{let i=Xt(o.screenshotData,"screenshot.png");A.append("screenshot",i)}catch(i){console.error("Failed to convert screenshot to file",i)}o.recordingEvents&&o.recordingEvents.length>0&&A.append("events",JSON.stringify(o.recordingEvents)),o.externalFile&&A.append("attachement",o.externalFile);let a=await Ge(A,m,{apiUrl:c,apiKey:g,authToken:f,tenantId:w});console.log("res",a);let s="Thank you for your feedback!";m==="bug"?s="Thank you! The bug report has been submitted.":m==="feature"&&(s="Thank you! The feature request has been submitted."),De.success("Success",{description:s}),z()}catch(f){console.error("API submission error:",f),De.error("Submission Failed",{description:"An error occurred while submitting your feedback. Please try again."})}finally{q(!1)}}},l=r=>{T[r]&&X(f=>({...f,[r]:void 0}))};return e?qt(_t,{children:[xe(Oe,{buttonRef:E,onClick:()=>$(!b)}),xe(We,{panelRef:d,isOpen:b,onClose:()=>$(!1),onOpenModal:B}),xe(He,{activeModal:m,onClose:z,onSubmit:ne,userEmail:t?.email_id||"",submitting:G,rating:y,setRating:C,hoverRating:D,setHoverRating:W,title:L,setTitle:K,description:R,setDescription:J,screenshotData:o.screenshotData,setScreenshotData:o.setScreenshotData,recordingBlob:o.recordingBlob,onRemoveRecording:()=>{o.setRecordingBlob(null),o.setRecordingUrl(null)},onPreviewRecording:()=>setTimeout(()=>j(!0),100),externalFile:o.externalFile,setExternalFile:o.setExternalFile,fileInputRef:F,handleFileChange:o.handleFileChange,onCaptureScreenshot:o.captureScreenshot,onStartRecording:o.startRecording,errors:T,onClearError:l,priorities:S,selectedPriorityId:H,setSelectedPriorityId:Y}),xe(Ye,{isRecording:o.isRecording,onStopRecording:o.stopRecording}),xe(Ke,{isOpen:o.editorOpen,capturedImg:o.capturedImg||"",onClose:()=>{o.setEditorOpen(!1),v(o.prevActiveModal.current||"general")},onSave:r=>{o.setScreenshotData(r),o.setEditorOpen(!1),v(o.prevActiveModal.current||"general"),De.success("Screenshot attached successfully!")}}),xe(Ne,{isOpen:P,onClose:()=>j(!1),events:o.recordingEvents})]}):null};export{Yt as FeedbackWidget,Ne as RRWebPlayerModal,Ne as SessionRecordingPreview};
|