@mindbowser_inc/ui-feedback-plugin 0.1.0 → 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.
- package/README.md +61 -40
- package/dist/index.d.ts +30 -35
- package/dist/s3-4V57aJiZ.mjs +66 -0
- package/dist/s3-4V57aJiZ.mjs.map +1 -0
- package/dist/ui-feedback-plugin.es.js +158 -173
- package/dist/ui-feedback-plugin.es.js.map +1 -1
- package/dist/ui-feedback-plugin.umd.js +120 -120
- package/dist/ui-feedback-plugin.umd.js.map +1 -1
- package/package.json +1 -1
- package/dist/s3-DkzKoz0A.mjs +0 -32
- package/dist/s3-DkzKoz0A.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,9 +8,7 @@ A drop-in floating widget for collecting annotated UI feedback on web prototypes
|
|
|
8
8
|
- Full-page screenshots with annotations
|
|
9
9
|
- Comment threads with replies
|
|
10
10
|
- Open / Resolved filter
|
|
11
|
-
-
|
|
12
|
-
- Firebase **or** Supabase backend
|
|
13
|
-
- S3 screenshot storage via a backend proxy
|
|
11
|
+
- Three fully independent backend options
|
|
14
12
|
|
|
15
13
|
---
|
|
16
14
|
|
|
@@ -22,22 +20,34 @@ npm install @mindbowser_inc/ui-feedback-plugin
|
|
|
22
20
|
|
|
23
21
|
---
|
|
24
22
|
|
|
25
|
-
##
|
|
23
|
+
## Backends
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
Choose **one** backend. They are fully independent — no mixing required.
|
|
26
|
+
|
|
27
|
+
| Backend | Database | Screenshot storage | Real-time |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| **Firebase** | Firestore | Firebase Storage | Yes (onSnapshot) |
|
|
30
|
+
| **Supabase** | Postgres | Supabase Storage | Yes (Realtime) |
|
|
31
|
+
| **AWS S3** | JSON files in S3 | S3 | Polling (5s default) |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Firebase
|
|
36
|
+
|
|
37
|
+
Firestore stores comments and replies. Firebase Storage holds screenshots.
|
|
28
38
|
|
|
29
39
|
```js
|
|
30
40
|
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
31
41
|
|
|
32
42
|
initFeedbackPlugin({
|
|
33
|
-
|
|
43
|
+
backend: {
|
|
34
44
|
provider: 'firebase',
|
|
35
|
-
apiKey:
|
|
36
|
-
authDomain:
|
|
37
|
-
projectId:
|
|
38
|
-
storageBucket:
|
|
45
|
+
apiKey: process.env.FIREBASE_API_KEY,
|
|
46
|
+
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
|
47
|
+
projectId: process.env.FIREBASE_PROJECT_ID,
|
|
48
|
+
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
|
39
49
|
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
|
40
|
-
appId:
|
|
50
|
+
appId: process.env.FIREBASE_APP_ID,
|
|
41
51
|
},
|
|
42
52
|
projectKey: 'my-project',
|
|
43
53
|
})
|
|
@@ -45,24 +55,28 @@ initFeedbackPlugin({
|
|
|
45
55
|
|
|
46
56
|
> The Firebase API key is a **public identifier**, not a secret. Secure your data with [Firebase Security Rules](https://firebase.google.com/docs/rules).
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Supabase
|
|
61
|
+
|
|
62
|
+
Postgres stores comments and replies. Supabase Storage holds screenshots.
|
|
49
63
|
|
|
50
64
|
```js
|
|
51
65
|
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
52
66
|
|
|
53
67
|
initFeedbackPlugin({
|
|
54
|
-
|
|
68
|
+
backend: {
|
|
55
69
|
provider: 'supabase',
|
|
56
|
-
url:
|
|
70
|
+
url: process.env.SUPABASE_URL,
|
|
57
71
|
anonKey: process.env.SUPABASE_ANON_KEY,
|
|
58
72
|
},
|
|
59
73
|
projectKey: 'my-project',
|
|
60
74
|
})
|
|
61
75
|
```
|
|
62
76
|
|
|
63
|
-
> The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security)
|
|
77
|
+
> The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security).
|
|
64
78
|
|
|
65
|
-
**
|
|
79
|
+
**Required SQL** — run this in your Supabase SQL editor once:
|
|
66
80
|
|
|
67
81
|
```sql
|
|
68
82
|
create table if not exists ufp_comments (
|
|
@@ -76,7 +90,6 @@ create table if not exists ufp_comments (
|
|
|
76
90
|
resolved boolean default false,
|
|
77
91
|
created_at bigint not null
|
|
78
92
|
);
|
|
79
|
-
|
|
80
93
|
create table if not exists ufp_replies (
|
|
81
94
|
id uuid primary key default gen_random_uuid(),
|
|
82
95
|
comment_id uuid not null references ufp_comments(id) on delete cascade,
|
|
@@ -84,19 +97,16 @@ create table if not exists ufp_replies (
|
|
|
84
97
|
text text not null,
|
|
85
98
|
created_at bigint not null
|
|
86
99
|
);
|
|
87
|
-
|
|
88
100
|
alter table ufp_comments enable row level security;
|
|
89
101
|
alter table ufp_replies enable row level security;
|
|
90
102
|
create policy "ufp_comments_all" on ufp_comments for all using (true) with check (true);
|
|
91
103
|
create policy "ufp_replies_all" on ufp_replies for all using (true) with check (true);
|
|
92
|
-
|
|
93
104
|
grant select, insert, update, delete on ufp_comments to anon;
|
|
94
105
|
grant select, insert, update, delete on ufp_replies to anon;
|
|
95
|
-
|
|
96
106
|
alter publication supabase_realtime add table ufp_comments;
|
|
97
107
|
```
|
|
98
108
|
|
|
99
|
-
Then create a
|
|
109
|
+
Then in **Storage**, create a public bucket named `ufp-screenshots` and add:
|
|
100
110
|
|
|
101
111
|
```sql
|
|
102
112
|
create policy "ufp_storage_insert" on storage.objects for insert to anon with check (bucket_id = 'ufp-screenshots');
|
|
@@ -106,27 +116,39 @@ create policy "ufp_storage_update" on storage.objects for update to anon using (
|
|
|
106
116
|
|
|
107
117
|
---
|
|
108
118
|
|
|
109
|
-
## S3
|
|
119
|
+
## AWS S3
|
|
120
|
+
|
|
121
|
+
Everything — comments, replies, and screenshots — lives in S3 as JSON/image files. No Firebase or Supabase needed.
|
|
110
122
|
|
|
111
|
-
|
|
123
|
+
**AWS credentials must never appear in frontend code.** This adapter calls your own backend API, which holds the credentials and talks to S3.
|
|
112
124
|
|
|
113
125
|
```js
|
|
126
|
+
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
127
|
+
|
|
114
128
|
initFeedbackPlugin({
|
|
115
|
-
|
|
116
|
-
provider:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
screenshots: {
|
|
121
|
-
provider: 's3',
|
|
122
|
-
uploadEndpoint: 'https://your-api.com/upload-screenshot',
|
|
123
|
-
headers: { Authorization: 'Bearer your-internal-token' },
|
|
129
|
+
backend: {
|
|
130
|
+
provider: 's3',
|
|
131
|
+
apiUrl: 'https://your-api.com/ufp', // your backend base URL
|
|
132
|
+
headers: { Authorization: 'Bearer your-internal-token' },
|
|
133
|
+
pollInterval: 5000, // ms between comment refreshes (default: 5000)
|
|
124
134
|
},
|
|
125
135
|
projectKey: 'my-project',
|
|
126
136
|
})
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
Your
|
|
139
|
+
**Your backend must expose these endpoints:**
|
|
140
|
+
|
|
141
|
+
| Method | Path | Body | Returns |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| GET | `/comments?pageUrl=...&projectKey=...` | — | `FeedbackComment[]` |
|
|
144
|
+
| POST | `/comments` | `FeedbackComment` (no id) | `{ id: string }` |
|
|
145
|
+
| PUT | `/comments/:id` | `Partial<FeedbackComment>` | `200` |
|
|
146
|
+
| DELETE | `/comments/:id` | — | `200` |
|
|
147
|
+
| GET | `/replies?commentId=...` | — | `Reply[]` |
|
|
148
|
+
| POST | `/replies` | `Reply` (no id) | `{ id: string }` |
|
|
149
|
+
| POST | `/screenshots` | `{ commentId, imageData }` (base64) | `{ url: string }` |
|
|
150
|
+
|
|
151
|
+
A full Node.js/Express reference server is included as comments inside [`src/adapters/s3.ts`](src/adapters/s3.ts).
|
|
130
152
|
|
|
131
153
|
---
|
|
132
154
|
|
|
@@ -134,23 +156,22 @@ Your endpoint receives `POST { commentId: string, imageData: string }` (base64)
|
|
|
134
156
|
|
|
135
157
|
```ts
|
|
136
158
|
initFeedbackPlugin({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
theme?: { primaryColor?: string } // default: '#6366f1'
|
|
159
|
+
backend: FirebaseProviderConfig | SupabaseProviderConfig | S3ProviderConfig // required
|
|
160
|
+
projectKey?: string // namespace comments per project (default: 'default')
|
|
161
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
162
|
+
theme?: { primaryColor?: string } // default: '#6366f1'
|
|
142
163
|
})
|
|
143
164
|
```
|
|
144
165
|
|
|
145
166
|
---
|
|
146
167
|
|
|
147
|
-
##
|
|
168
|
+
## Plain HTML (UMD / CDN)
|
|
148
169
|
|
|
149
170
|
```html
|
|
150
171
|
<script src="https://unpkg.com/@mindbowser_inc/ui-feedback-plugin/dist/ui-feedback-plugin.umd.js"></script>
|
|
151
172
|
<script>
|
|
152
173
|
UIFeedbackPlugin.initFeedbackPlugin({
|
|
153
|
-
|
|
174
|
+
backend: { provider: 'supabase', url: '...', anonKey: '...' },
|
|
154
175
|
projectKey: 'my-project',
|
|
155
176
|
})
|
|
156
177
|
</script>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
declare type
|
|
1
|
+
declare type BackendConfig = FirebaseProviderConfig | SupabaseProviderConfig | S3ProviderConfig;
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Firebase backend — Firestore for comments/replies, Firebase Storage for screenshots.
|
|
5
|
+
* The API key is a public identifier, not a secret. Secure data with Firebase Security Rules.
|
|
6
|
+
*/
|
|
3
7
|
declare interface FirebaseProviderConfig {
|
|
4
8
|
provider: 'firebase';
|
|
5
|
-
/**
|
|
6
|
-
* Firebase API key. This is a public identifier — not a secret.
|
|
7
|
-
* Secure your data with Firebase Security Rules.
|
|
8
|
-
* Store in an environment variable (VITE_FIREBASE_API_KEY, REACT_APP_FIREBASE_API_KEY, etc.)
|
|
9
|
-
*/
|
|
10
9
|
apiKey: string;
|
|
11
10
|
authDomain: string;
|
|
12
11
|
projectId: string;
|
|
13
|
-
/** Required
|
|
12
|
+
/** Required for screenshot storage. */
|
|
14
13
|
storageBucket?: string;
|
|
15
14
|
messagingSenderId?: string;
|
|
16
15
|
appId?: string;
|
|
@@ -19,15 +18,14 @@ declare interface FirebaseProviderConfig {
|
|
|
19
18
|
export declare function initFeedbackPlugin(config: PluginConfig): () => void;
|
|
20
19
|
|
|
21
20
|
export declare interface PluginConfig {
|
|
22
|
-
/** Database provider for storing comments and replies. */
|
|
23
|
-
database: DatabaseConfig;
|
|
24
21
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* Storage backend. Pick one:
|
|
23
|
+
* - `firebase` — Firestore + Firebase Storage
|
|
24
|
+
* - `supabase` — Postgres + Supabase Storage
|
|
25
|
+
* - `s3` — Everything in AWS S3 via your backend API (no Firebase/Supabase needed)
|
|
28
26
|
*/
|
|
29
|
-
|
|
30
|
-
/** Namespace comments by project
|
|
27
|
+
backend: BackendConfig;
|
|
28
|
+
/** Namespace comments by project (default: 'default'). */
|
|
31
29
|
projectKey?: string;
|
|
32
30
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
33
31
|
theme?: {
|
|
@@ -36,37 +34,34 @@ export declare interface PluginConfig {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* S3
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* AWS S3 backend — all data (comments, replies, screenshots) stored in S3.
|
|
38
|
+
* No Firebase or Supabase needed.
|
|
39
|
+
*
|
|
40
|
+
* AWS credentials MUST stay on your server — this adapter calls your backend API,
|
|
41
|
+
* which in turn reads/writes S3. See src/adapters/s3.ts for the required API contract
|
|
42
|
+
* and a full Node.js/Express reference implementation.
|
|
42
43
|
*/
|
|
43
|
-
declare interface
|
|
44
|
+
declare interface S3ProviderConfig {
|
|
44
45
|
provider: 's3';
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
/** Optional headers (e.g.
|
|
46
|
+
/** Base URL of your backend API (e.g. https://your-api.com/ufp). No trailing slash. */
|
|
47
|
+
apiUrl: string;
|
|
48
|
+
/** Optional headers sent with every request (e.g. { Authorization: 'Bearer token' }). */
|
|
48
49
|
headers?: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* How often (ms) to poll for new comments. S3 has no real-time push.
|
|
52
|
+
* Default: 5000 (5 seconds).
|
|
53
|
+
*/
|
|
54
|
+
pollInterval?: number;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
58
|
+
* Supabase backend — Postgres for comments/replies, Supabase Storage for screenshots.
|
|
59
|
+
* The anonKey is a public key by design. Secure data with Row Level Security (RLS) policies.
|
|
60
|
+
* See src/adapters/supabase.ts for the required SQL setup.
|
|
55
61
|
*/
|
|
56
|
-
declare type ScreenshotConfig = {
|
|
57
|
-
provider: 'firebase';
|
|
58
|
-
} | {
|
|
59
|
-
provider: 'supabase';
|
|
60
|
-
} | S3ScreenshotConfig;
|
|
61
|
-
|
|
62
62
|
declare interface SupabaseProviderConfig {
|
|
63
63
|
provider: 'supabase';
|
|
64
64
|
url: string;
|
|
65
|
-
/**
|
|
66
|
-
* Supabase anon key. This is a public key — not a secret.
|
|
67
|
-
* Secure your data with Supabase Row Level Security (RLS) policies.
|
|
68
|
-
* Store in an environment variable.
|
|
69
|
-
*/
|
|
70
65
|
anonKey: string;
|
|
71
66
|
}
|
|
72
67
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var c = Object.defineProperty;
|
|
2
|
+
var l = (r, e, t) => e in r ? c(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
|
|
3
|
+
var n = (r, e, t) => l(r, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
class d {
|
|
5
|
+
constructor(e, t = {}, s = 5e3) {
|
|
6
|
+
n(this, "base");
|
|
7
|
+
n(this, "headers");
|
|
8
|
+
n(this, "pollInterval");
|
|
9
|
+
this.base = e.replace(/\/$/, ""), this.headers = t, this.pollInterval = s;
|
|
10
|
+
}
|
|
11
|
+
async req(e, t, s) {
|
|
12
|
+
const a = await fetch(`${this.base}${t}`, {
|
|
13
|
+
method: e,
|
|
14
|
+
headers: { "Content-Type": "application/json", ...this.headers },
|
|
15
|
+
body: s !== void 0 ? JSON.stringify(s) : void 0
|
|
16
|
+
});
|
|
17
|
+
if (!a.ok)
|
|
18
|
+
throw new Error(`[ui-feedback-plugin] S3 API ${e} ${t} → ${a.status} ${a.statusText}`);
|
|
19
|
+
if (e !== "DELETE")
|
|
20
|
+
return a.json();
|
|
21
|
+
}
|
|
22
|
+
async addComment(e) {
|
|
23
|
+
return (await this.req("POST", "/comments", e)).id;
|
|
24
|
+
}
|
|
25
|
+
async getComments(e, t) {
|
|
26
|
+
const s = new URLSearchParams({ pageUrl: e, projectKey: t });
|
|
27
|
+
return this.req("GET", `/comments?${s}`);
|
|
28
|
+
}
|
|
29
|
+
async updateComment(e, t) {
|
|
30
|
+
await this.req("PUT", `/comments/${e}`, t);
|
|
31
|
+
}
|
|
32
|
+
async deleteComment(e) {
|
|
33
|
+
await this.req("DELETE", `/comments/${e}`);
|
|
34
|
+
}
|
|
35
|
+
async addReply(e) {
|
|
36
|
+
return (await this.req("POST", "/replies", e)).id;
|
|
37
|
+
}
|
|
38
|
+
async getReplies(e) {
|
|
39
|
+
return this.req("GET", `/replies?commentId=${encodeURIComponent(e)}`);
|
|
40
|
+
}
|
|
41
|
+
subscribeToComments(e, t, s) {
|
|
42
|
+
let a = !0;
|
|
43
|
+
const o = async () => {
|
|
44
|
+
if (a) {
|
|
45
|
+
try {
|
|
46
|
+
const i = await this.getComments(e, t);
|
|
47
|
+
a && s(i);
|
|
48
|
+
} catch (i) {
|
|
49
|
+
console.error("[ui-feedback-plugin] S3 poll error:", i);
|
|
50
|
+
}
|
|
51
|
+
a && setTimeout(o, this.pollInterval);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return o(), () => {
|
|
55
|
+
a = !1;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async uploadScreenshot(e, t) {
|
|
59
|
+
const s = t.includes(",") ? t.split(",")[1] : t;
|
|
60
|
+
return (await this.req("POST", "/screenshots", { commentId: e, imageData: s })).url;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
d as S3Adapter
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=s3-4V57aJiZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-4V57aJiZ.mjs","sources":["../src/adapters/s3.ts"],"sourcesContent":["/**\n * AWS S3 adapter — stores everything (comments, replies, screenshots) via your backend API.\n * AWS credentials live on YOUR server; this adapter only calls HTTP endpoints.\n *\n * ── Required API contract ─────────────────────────────────────────────────────\n *\n * GET {apiUrl}/comments?pageUrl=...&projectKey=... → FeedbackComment[]\n * POST {apiUrl}/comments → { id: string }\n * PUT {apiUrl}/comments/:id → 200\n * DELETE {apiUrl}/comments/:id → 200\n *\n * GET {apiUrl}/replies?commentId=... → Reply[]\n * POST {apiUrl}/replies → { id: string }\n *\n * POST {apiUrl}/screenshots → { url: string }\n * body: { commentId: string, imageData: string (base64) }\n *\n * ── Minimal Node.js / Express reference server ────────────────────────────────\n *\n * const express = require('express')\n * const { S3Client, PutObjectCommand, GetObjectCommand,\n * DeleteObjectCommand, ListObjectsV2Command } = require('@aws-sdk/client-s3')\n * const { v4: uuidv4 } = require('uuid')\n * const crypto = require('crypto')\n *\n * const s3 = new S3Client({ region: process.env.AWS_REGION })\n * const BUCKET = process.env.S3_BUCKET\n * const app = express()\n * app.use(express.json({ limit: '10mb' }))\n *\n * const hashUrl = (url) => crypto.createHash('md5').update(url).digest('hex')\n *\n * async function s3Get(key) {\n * const r = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }))\n * return JSON.parse(await r.Body.transformToString())\n * }\n * async function s3Put(key, data, contentType = 'application/json') {\n * const body = typeof data === 'string' ? data : JSON.stringify(data)\n * await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body, ContentType: contentType }))\n * }\n * async function s3List(prefix) {\n * const r = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix }))\n * return r.Contents ?? []\n * }\n *\n * // Comments\n * app.get('/ufp/comments', async (req, res) => {\n * const { pageUrl, projectKey } = req.query\n * const prefix = `comments/${projectKey}/${hashUrl(pageUrl)}/`\n * const items = await s3List(prefix)\n * const comments = await Promise.all(items.map(i => s3Get(i.Key)))\n * res.json(comments.sort((a, b) => b.createdAt - a.createdAt))\n * })\n *\n * app.post('/ufp/comments', async (req, res) => {\n * const comment = req.body\n * const id = uuidv4()\n * const key = `comments/${comment.projectKey}/${hashUrl(comment.pageUrl)}/${id}.json`\n * await s3Put(key, { id, ...comment })\n * res.json({ id })\n * })\n *\n * app.put('/ufp/comments/:id', async (req, res) => {\n * // Requires you to store a separate index: id → s3 key\n * // Simplest: scan prefix and find matching id\n * const { id } = req.params\n * const allPrefixes = await s3List('comments/')\n * const match = allPrefixes.find(i => i.Key.endsWith(`/${id}.json`))\n * if (!match) return res.status(404).end()\n * const existing = await s3Get(match.Key)\n * await s3Put(match.Key, { ...existing, ...req.body })\n * res.status(200).end()\n * })\n *\n * app.delete('/ufp/comments/:id', async (req, res) => {\n * const { id } = req.params\n * const allItems = await s3List('comments/')\n * const match = allItems.find(i => i.Key.endsWith(`/${id}.json`))\n * if (match) await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: match.Key }))\n * // Also delete replies\n * const replies = await s3List(`replies/${id}/`)\n * await Promise.all(replies.map(i => s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: i.Key }))))\n * res.status(200).end()\n * })\n *\n * // Replies\n * app.get('/ufp/replies', async (req, res) => {\n * const { commentId } = req.query\n * const items = await s3List(`replies/${commentId}/`)\n * const replies = await Promise.all(items.map(i => s3Get(i.Key)))\n * res.json(replies.sort((a, b) => a.createdAt - b.createdAt))\n * })\n *\n * app.post('/ufp/replies', async (req, res) => {\n * const reply = req.body\n * const id = uuidv4()\n * await s3Put(`replies/${reply.commentId}/${id}.json`, { id, ...reply })\n * res.json({ id })\n * })\n *\n * // Screenshots\n * app.post('/ufp/screenshots', async (req, res) => {\n * const { commentId, imageData } = req.body\n * const buffer = Buffer.from(imageData, 'base64')\n * const key = `screenshots/${commentId}.png`\n * await s3.send(new PutObjectCommand({\n * Bucket: BUCKET, Key: key, Body: buffer,\n * ContentType: 'image/png',\n * }))\n * res.json({ url: `https://${BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}` })\n * })\n *\n * app.listen(3001, () => console.log('UFP S3 API running on :3001'))\n */\nimport type { FeedbackComment, Reply, DatabaseAdapter, ScreenshotAdapter } from '../core/types'\n\nexport class S3Adapter implements DatabaseAdapter, ScreenshotAdapter {\n private base: string\n private headers: Record<string, string>\n private pollInterval: number\n\n constructor(apiUrl: string, headers: Record<string, string> = {}, pollInterval = 5000) {\n this.base = apiUrl.replace(/\\/$/, '')\n this.headers = headers\n this.pollInterval = pollInterval\n }\n\n private async req<T>(method: string, path: string, body?: unknown): Promise<T> {\n const res = await fetch(`${this.base}${path}`, {\n method,\n headers: { 'Content-Type': 'application/json', ...this.headers },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n if (!res.ok) {\n throw new Error(`[ui-feedback-plugin] S3 API ${method} ${path} → ${res.status} ${res.statusText}`)\n }\n // DELETE returns 200 with no body\n if (method === 'DELETE') return undefined as T\n return res.json() as Promise<T>\n }\n\n async addComment(comment: Omit<FeedbackComment, 'id'>): Promise<string> {\n const data = await this.req<{ id: string }>('POST', '/comments', comment)\n return data.id\n }\n\n async getComments(pageUrl: string, projectKey: string): Promise<FeedbackComment[]> {\n const params = new URLSearchParams({ pageUrl, projectKey })\n return this.req<FeedbackComment[]>('GET', `/comments?${params}`)\n }\n\n async updateComment(id: string, data: Partial<FeedbackComment>): Promise<void> {\n await this.req('PUT', `/comments/${id}`, data)\n }\n\n async deleteComment(id: string): Promise<void> {\n await this.req('DELETE', `/comments/${id}`)\n }\n\n async addReply(reply: Omit<Reply, 'id'>): Promise<string> {\n const data = await this.req<{ id: string }>('POST', '/replies', reply)\n return data.id\n }\n\n async getReplies(commentId: string): Promise<Reply[]> {\n return this.req<Reply[]>('GET', `/replies?commentId=${encodeURIComponent(commentId)}`)\n }\n\n subscribeToComments(\n pageUrl: string,\n projectKey: string,\n callback: (comments: FeedbackComment[]) => void\n ): () => void {\n let active = true\n\n const poll = async () => {\n if (!active) return\n try {\n const comments = await this.getComments(pageUrl, projectKey)\n if (active) callback(comments)\n } catch (err) {\n console.error('[ui-feedback-plugin] S3 poll error:', err)\n }\n if (active) setTimeout(poll, this.pollInterval)\n }\n\n poll()\n return () => { active = false }\n }\n\n async uploadScreenshot(commentId: string, dataUrl: string): Promise<string> {\n const imageData = dataUrl.includes(',') ? dataUrl.split(',')[1] : dataUrl\n const data = await this.req<{ url: string }>('POST', '/screenshots', { commentId, imageData })\n return data.url\n }\n}\n"],"names":["S3Adapter","apiUrl","headers","pollInterval","__publicField","method","path","body","res","comment","pageUrl","projectKey","params","id","data","reply","commentId","callback","active","poll","comments","err","dataUrl","imageData"],"mappings":";;;AAoHO,MAAMA,EAAwD;AAAA,EAKnE,YAAYC,GAAgBC,IAAkC,CAAA,GAAIC,IAAe,KAAM;AAJ/E,IAAAC,EAAA;AACA,IAAAA,EAAA;AACA,IAAAA,EAAA;AAGN,SAAK,OAAOH,EAAO,QAAQ,OAAO,EAAE,GACpC,KAAK,UAAUC,GACf,KAAK,eAAeC;AAAA,EACtB;AAAA,EAEA,MAAc,IAAOE,GAAgBC,GAAcC,GAA4B;AAC7E,UAAMC,IAAM,MAAM,MAAM,GAAG,KAAK,IAAI,GAAGF,CAAI,IAAI;AAAA,MAC7C,QAAAD;AAAA,MACA,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,QAAA;AAAA,MACvD,MAAME,MAAS,SAAY,KAAK,UAAUA,CAAI,IAAI;AAAA,IAAA,CACnD;AACD,QAAI,CAACC,EAAI;AACP,YAAM,IAAI,MAAM,+BAA+BH,CAAM,IAAIC,CAAI,MAAME,EAAI,MAAM,IAAIA,EAAI,UAAU,EAAE;AAGnG,QAAIH,MAAW;AACf,aAAOG,EAAI,KAAA;AAAA,EACb;AAAA,EAEA,MAAM,WAAWC,GAAuD;AAEtE,YADa,MAAM,KAAK,IAAoB,QAAQ,aAAaA,CAAO,GAC5D;AAAA,EACd;AAAA,EAEA,MAAM,YAAYC,GAAiBC,GAAgD;AACjF,UAAMC,IAAS,IAAI,gBAAgB,EAAE,SAAAF,GAAS,YAAAC,GAAY;AAC1D,WAAO,KAAK,IAAuB,OAAO,aAAaC,CAAM,EAAE;AAAA,EACjE;AAAA,EAEA,MAAM,cAAcC,GAAYC,GAA+C;AAC7E,UAAM,KAAK,IAAI,OAAO,aAAaD,CAAE,IAAIC,CAAI;AAAA,EAC/C;AAAA,EAEA,MAAM,cAAcD,GAA2B;AAC7C,UAAM,KAAK,IAAI,UAAU,aAAaA,CAAE,EAAE;AAAA,EAC5C;AAAA,EAEA,MAAM,SAASE,GAA2C;AAExD,YADa,MAAM,KAAK,IAAoB,QAAQ,YAAYA,CAAK,GACzD;AAAA,EACd;AAAA,EAEA,MAAM,WAAWC,GAAqC;AACpD,WAAO,KAAK,IAAa,OAAO,sBAAsB,mBAAmBA,CAAS,CAAC,EAAE;AAAA,EACvF;AAAA,EAEA,oBACEN,GACAC,GACAM,GACY;AACZ,QAAIC,IAAS;AAEb,UAAMC,IAAO,YAAY;AACvB,UAAKD,GACL;AAAA,YAAI;AACF,gBAAME,IAAW,MAAM,KAAK,YAAYV,GAASC,CAAU;AAC3D,UAAIO,OAAiBE,CAAQ;AAAA,QAC/B,SAASC,GAAK;AACZ,kBAAQ,MAAM,uCAAuCA,CAAG;AAAA,QAC1D;AACA,QAAIH,KAAQ,WAAWC,GAAM,KAAK,YAAY;AAAA;AAAA,IAChD;AAEA,WAAAA,EAAA,GACO,MAAM;AAAE,MAAAD,IAAS;AAAA,IAAM;AAAA,EAChC;AAAA,EAEA,MAAM,iBAAiBF,GAAmBM,GAAkC;AAC1E,UAAMC,IAAYD,EAAQ,SAAS,GAAG,IAAIA,EAAQ,MAAM,GAAG,EAAE,CAAC,IAAIA;AAElE,YADa,MAAM,KAAK,IAAqB,QAAQ,gBAAgB,EAAE,WAAAN,GAAW,WAAAO,GAAW,GACjF;AAAA,EACd;AACF;"}
|