@heedb/web-sdk 0.1.0 → 0.1.2
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 +452 -17
- package/dist/widget.js +53 -8
- package/package.json +2 -1
- package/widget.js +129 -0
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# @heedb/web-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight (~12 KB) feedback widget that drops into any website. Customers can send messages, make privacy requests, and view their conversation history — all through email-based threads. No login required.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Dashboard](https://heedb.com) · [GitHub](https://github.com/TheAleSch/heedb)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
### CDN (recommended)
|
|
12
|
+
|
|
13
|
+
The fastest way — no build step, no dependencies. Add one line before `</body>`:
|
|
6
14
|
|
|
7
15
|
```html
|
|
8
16
|
<script
|
|
9
|
-
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/
|
|
17
|
+
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/widget.js"
|
|
10
18
|
data-api-key="YOUR_API_KEY"
|
|
11
19
|
data-host="https://heedb.com"
|
|
12
20
|
></script>
|
|
13
21
|
```
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
> **Important:** `data-host` is required when loading from a CDN. It tells the widget where to send API requests. Omit it only when the script is served from the same domain as your Heedb instance.
|
|
24
|
+
|
|
25
|
+
A floating chat button appears in the bottom-right corner. That's it.
|
|
26
|
+
|
|
27
|
+
### npm
|
|
16
28
|
|
|
17
29
|
```bash
|
|
18
30
|
npm install @heedb/web-sdk
|
|
@@ -21,29 +33,452 @@ npm install @heedb/web-sdk
|
|
|
21
33
|
```ts
|
|
22
34
|
import { Heedb } from "@heedb/web-sdk";
|
|
23
35
|
|
|
36
|
+
Heedb.init({ apiKey: "YOUR_API_KEY" });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This dynamically loads the widget and attaches it to the page. Works with React, Next.js, Vue, Svelte, or any framework.
|
|
40
|
+
|
|
41
|
+
### Self-hosted
|
|
42
|
+
|
|
43
|
+
If you run your own Heedb instance, serve `widget.js` from your domain:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script src="https://your-instance.com/widget.js" data-api-key="YOUR_API_KEY"></script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The widget auto-detects the API host from the script's origin — no `data-host` needed.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Environment variables
|
|
54
|
+
|
|
55
|
+
Add these to your `.env` (or equivalent):
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
# Public — safe for client-side code, used in data-api-key or Heedb.init()
|
|
59
|
+
NEXT_PUBLIC_HEEDB_API_KEY=your_api_key_here
|
|
60
|
+
|
|
61
|
+
# Private — server-side only, used to generate userHash
|
|
62
|
+
# NEVER expose this in client-side code, bundle, or git
|
|
63
|
+
HEEDB_WIDGET_SECRET=your_widget_secret_here
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both values are in your [dashboard settings](https://heedb.com/app/settings).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Identify users
|
|
71
|
+
|
|
72
|
+
By default, the widget shows a form asking for name, email, and message. If the user is already logged in to your app, you can skip that step.
|
|
73
|
+
|
|
74
|
+
There are three levels:
|
|
75
|
+
|
|
76
|
+
### 1. Anonymous (default)
|
|
77
|
+
|
|
78
|
+
No `init()` call needed. The widget shows the full contact form. Good for marketing sites, landing pages, or anywhere users aren't logged in.
|
|
79
|
+
|
|
80
|
+
### 2. Identified (name + email)
|
|
81
|
+
|
|
82
|
+
Pre-fills the form and hides the name/email fields. The user only sees the message box. **No server-side code required.**
|
|
83
|
+
|
|
84
|
+
```ts
|
|
24
85
|
Heedb.init({
|
|
25
86
|
apiKey: "YOUR_API_KEY",
|
|
26
|
-
email:
|
|
27
|
-
name:
|
|
28
|
-
userHash: serverHash, // optional — HMAC-SHA256 for verified identity
|
|
87
|
+
email: "jane@example.com",
|
|
88
|
+
name: "Jane",
|
|
29
89
|
});
|
|
30
90
|
```
|
|
31
91
|
|
|
32
|
-
|
|
92
|
+
Use this when you know the user's identity but don't need to show them their conversation history.
|
|
93
|
+
|
|
94
|
+
### 3. Verified (name + email + userHash)
|
|
95
|
+
|
|
96
|
+
Same as identified, plus unlocks the **Messages** tab where the user can view their previous conversation threads.
|
|
97
|
+
|
|
98
|
+
**This requires a server-generated HMAC.** The `userHash` must be computed on your backend and passed to the frontend — never generate it client-side, as that would expose your Widget Secret.
|
|
33
99
|
|
|
34
|
-
|
|
100
|
+
```ts
|
|
101
|
+
// Client-side — after receiving userHash from your server
|
|
102
|
+
Heedb.init({
|
|
103
|
+
apiKey: "YOUR_API_KEY",
|
|
104
|
+
email: "jane@example.com",
|
|
105
|
+
name: "Jane",
|
|
106
|
+
userHash: serverGeneratedHash, // from your backend
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Only pass `userHash` when a user is authenticated.** For logged-out users, either call `init()` without it (identified) or don't call `init()` at all (anonymous).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Generate the userHash (server-side)
|
|
115
|
+
|
|
116
|
+
The `userHash` is an HMAC-SHA256 of the user's email, signed with your **Widget Secret**. It proves your server vouches for this user's identity.
|
|
117
|
+
|
|
118
|
+
**The flow:**
|
|
119
|
+
1. User logs in to your app
|
|
120
|
+
2. Your server computes `HMAC-SHA256(email, WIDGET_SECRET)` → `userHash`
|
|
121
|
+
3. Your server passes `userHash` to the frontend (via props, API response, or server-rendered HTML)
|
|
122
|
+
4. The frontend calls `Heedb.init({ email, userHash })`
|
|
123
|
+
|
|
124
|
+
**Never compute the hash client-side — the Widget Secret must stay on your server.**
|
|
125
|
+
|
|
126
|
+
### Node.js
|
|
35
127
|
|
|
36
128
|
```js
|
|
37
129
|
const crypto = require("crypto");
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
130
|
+
|
|
131
|
+
function heedbUserHash(email) {
|
|
132
|
+
return crypto
|
|
133
|
+
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET)
|
|
134
|
+
.update(email)
|
|
135
|
+
.digest("hex");
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Python
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import hmac, hashlib, os
|
|
143
|
+
|
|
144
|
+
def heedb_user_hash(email: str) -> str:
|
|
145
|
+
secret = os.environ["HEEDB_WIDGET_SECRET"].encode()
|
|
146
|
+
return hmac.new(secret, email.encode(), hashlib.sha256).hexdigest()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### PHP
|
|
150
|
+
|
|
151
|
+
```php
|
|
152
|
+
function heedbUserHash(string $email): string {
|
|
153
|
+
return hash_hmac('sha256', $email, getenv('HEEDB_WIDGET_SECRET'));
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Ruby
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "openssl"
|
|
161
|
+
|
|
162
|
+
def heedb_user_hash(email)
|
|
163
|
+
OpenSSL::HMAC.hexdigest("sha256", ENV["HEEDB_WIDGET_SECRET"], email)
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Go
|
|
168
|
+
|
|
169
|
+
```go
|
|
170
|
+
import (
|
|
171
|
+
"crypto/hmac"
|
|
172
|
+
"crypto/sha256"
|
|
173
|
+
"encoding/hex"
|
|
174
|
+
"os"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
func heedbUserHash(email string) string {
|
|
178
|
+
h := hmac.New(sha256.New, []byte(os.Getenv("HEEDB_WIDGET_SECRET")))
|
|
179
|
+
h.Write([]byte(email))
|
|
180
|
+
return hex.EncodeToString(h.Sum(nil))
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Framework examples
|
|
187
|
+
|
|
188
|
+
### Next.js (App Router) — full example with verified identity
|
|
189
|
+
|
|
190
|
+
This is the recommended pattern. The hash is computed in a server component and passed to a client component.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// app/components/HeedbWidget.server.tsx — Server Component
|
|
194
|
+
import { auth } from "@/lib/auth"; // your auth library
|
|
195
|
+
import { headers } from "next/headers";
|
|
196
|
+
import { createHmac } from "crypto";
|
|
197
|
+
import HeedbWidgetClient from "./HeedbWidget.client";
|
|
198
|
+
|
|
199
|
+
export default async function HeedbWidget() {
|
|
200
|
+
let email: string | undefined;
|
|
201
|
+
let name: string | undefined;
|
|
202
|
+
let userHash: string | undefined;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
206
|
+
if (session?.user?.email) {
|
|
207
|
+
email = session.user.email;
|
|
208
|
+
name = session.user.name ?? undefined;
|
|
209
|
+
// Compute HMAC on the server — secret never reaches the client
|
|
210
|
+
userHash = createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
|
|
211
|
+
.update(email)
|
|
212
|
+
.digest("hex");
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// No session — widget will work anonymously
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return <HeedbWidgetClient email={email} name={name} userHash={userHash} />;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
// app/components/HeedbWidget.client.tsx — Client Component
|
|
224
|
+
"use client";
|
|
225
|
+
|
|
226
|
+
import { useEffect } from "react";
|
|
227
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
228
|
+
|
|
229
|
+
export default function HeedbWidgetClient({ email, name, userHash }: {
|
|
230
|
+
email?: string;
|
|
231
|
+
name?: string;
|
|
232
|
+
userHash?: string;
|
|
233
|
+
}) {
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
Heedb.init({
|
|
236
|
+
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
|
|
237
|
+
email,
|
|
238
|
+
name,
|
|
239
|
+
userHash,
|
|
240
|
+
});
|
|
241
|
+
}, [email, name, userHash]);
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// app/layout.tsx
|
|
249
|
+
import HeedbWidget from "./components/HeedbWidget.server";
|
|
250
|
+
|
|
251
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
252
|
+
return (
|
|
253
|
+
<html>
|
|
254
|
+
<body>
|
|
255
|
+
{children}
|
|
256
|
+
<HeedbWidget />
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Next.js (Pages Router)
|
|
264
|
+
|
|
265
|
+
Compute the hash in `getServerSideProps` and pass it as a prop:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
// pages/_app.tsx
|
|
269
|
+
import { useEffect } from "react";
|
|
270
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
271
|
+
|
|
272
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (pageProps.heedbEmail) {
|
|
275
|
+
Heedb.init({
|
|
276
|
+
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
|
|
277
|
+
email: pageProps.heedbEmail,
|
|
278
|
+
name: pageProps.heedbName,
|
|
279
|
+
userHash: pageProps.heedbUserHash,
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
Heedb.init({ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY! });
|
|
283
|
+
}
|
|
284
|
+
}, [pageProps.heedbEmail]);
|
|
285
|
+
|
|
286
|
+
return <Component {...pageProps} />;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### React (Vite / CRA) — with API route for hash
|
|
291
|
+
|
|
292
|
+
When you don't have server components, fetch the hash from an API endpoint:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// Server: POST /api/heedb-hash
|
|
296
|
+
import crypto from "crypto";
|
|
297
|
+
export function handler(req, res) {
|
|
298
|
+
const { email } = req.body;
|
|
299
|
+
const hash = crypto
|
|
300
|
+
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
|
|
301
|
+
.update(email)
|
|
302
|
+
.digest("hex");
|
|
303
|
+
res.json({ userHash: hash });
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
// Client: App.tsx
|
|
309
|
+
import { useEffect } from "react";
|
|
310
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
311
|
+
|
|
312
|
+
function App() {
|
|
313
|
+
const user = useAuth(); // your auth hook
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!user) {
|
|
317
|
+
Heedb.init({ apiKey: import.meta.env.VITE_HEEDB_API_KEY });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fetch the hash from your server
|
|
322
|
+
fetch("/api/heedb-hash", {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify({ email: user.email }),
|
|
326
|
+
})
|
|
327
|
+
.then((r) => r.json())
|
|
328
|
+
.then(({ userHash }) => {
|
|
329
|
+
Heedb.init({
|
|
330
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
331
|
+
email: user.email,
|
|
332
|
+
name: user.name,
|
|
333
|
+
userHash,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}, [user]);
|
|
337
|
+
|
|
338
|
+
return <div>Your app</div>;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Vue
|
|
343
|
+
|
|
344
|
+
```vue
|
|
345
|
+
<script setup>
|
|
346
|
+
import { onMounted } from "vue";
|
|
347
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
348
|
+
|
|
349
|
+
const props = defineProps<{
|
|
350
|
+
email?: string;
|
|
351
|
+
name?: string;
|
|
352
|
+
userHash?: string;
|
|
353
|
+
}>();
|
|
354
|
+
|
|
355
|
+
onMounted(() => {
|
|
356
|
+
Heedb.init({
|
|
357
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
358
|
+
email: props.email,
|
|
359
|
+
name: props.name,
|
|
360
|
+
userHash: props.userHash,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
</script>
|
|
42
364
|
```
|
|
43
365
|
|
|
44
|
-
|
|
366
|
+
### Svelte
|
|
367
|
+
|
|
368
|
+
```svelte
|
|
369
|
+
<script>
|
|
370
|
+
import { onMount } from "svelte";
|
|
371
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
372
|
+
|
|
373
|
+
export let email = undefined;
|
|
374
|
+
export let name = undefined;
|
|
375
|
+
export let userHash = undefined;
|
|
376
|
+
|
|
377
|
+
onMount(() => {
|
|
378
|
+
Heedb.init({
|
|
379
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
380
|
+
email,
|
|
381
|
+
name,
|
|
382
|
+
userHash,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
</script>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Static HTML / WordPress / Webflow
|
|
389
|
+
|
|
390
|
+
Use the CDN script tag — no build tools needed:
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<script
|
|
394
|
+
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/widget.js"
|
|
395
|
+
data-api-key="YOUR_API_KEY"
|
|
396
|
+
data-host="https://heedb.com"
|
|
397
|
+
></script>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
For verified identity on static sites, you'll need a small server endpoint that returns the `userHash` — see the React + API route example above.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## What the widget does
|
|
405
|
+
|
|
406
|
+
The widget adds a floating button to your page with three tabs:
|
|
407
|
+
|
|
408
|
+
| Tab | Description | Requires |
|
|
409
|
+
|-----|-------------|----------|
|
|
410
|
+
| **Message** | Contact form — name, email, and a message. Creates a new support thread. Fields are hidden when the user is identified. | Nothing |
|
|
411
|
+
| **Privacy** | GDPR/privacy request form — data export, deletion, or opt-out. | Nothing |
|
|
412
|
+
| **Messages** | Conversation history — shows open threads. | Verified identity (`userHash`) |
|
|
413
|
+
|
|
414
|
+
**The full conversation loop:**
|
|
415
|
+
|
|
416
|
+
1. Customer submits a message via the widget
|
|
417
|
+
2. A thread appears in your [Heedb dashboard](https://heedb.com/app/threads)
|
|
418
|
+
3. You get an email notification
|
|
419
|
+
4. You reply from the dashboard — the customer receives an email
|
|
420
|
+
5. The customer replies to that email — it shows up in the dashboard
|
|
421
|
+
6. If the customer is verified, they can also see the full conversation in the widget's Messages tab
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Configuration reference
|
|
426
|
+
|
|
427
|
+
### Script tag attributes
|
|
428
|
+
|
|
429
|
+
| Attribute | Required | Description |
|
|
430
|
+
|-----------|----------|-------------|
|
|
431
|
+
| `data-api-key` | Yes | Your project's public API key |
|
|
432
|
+
| `data-host` | CDN: Yes. Self-hosted: No | API host URL. **Required when loading from CDN** (e.g. jsDelivr). When self-hosted, defaults to the script's origin. |
|
|
433
|
+
|
|
434
|
+
### `Heedb.init()` options
|
|
435
|
+
|
|
436
|
+
| Option | Type | Required | Description |
|
|
437
|
+
|--------|------|----------|-------------|
|
|
438
|
+
| `apiKey` | `string` | npm: Yes. Script tag: No | Your project's public API key. Script tag reads it from `data-api-key`. |
|
|
439
|
+
| `host` | `string` | No | API host URL. Defaults to `https://heedb.com`. |
|
|
440
|
+
| `email` | `string` | No | User's email — pre-fills the form and hides the email field |
|
|
441
|
+
| `name` | `string` | No | User's name — pre-fills the form and hides the name field |
|
|
442
|
+
| `userHash` | `string` | No | Server-generated HMAC-SHA256 hash — unlocks conversation history. **Must come from your server.** |
|
|
443
|
+
|
|
444
|
+
### Behavior by identity level
|
|
445
|
+
|
|
446
|
+
| What happens | Anonymous | Identified | Verified |
|
|
447
|
+
|---|---|---|---|
|
|
448
|
+
| `init()` called | No | Yes (email + name) | Yes (email + name + userHash) |
|
|
449
|
+
| Contact form visible | Yes (full form) | Yes (message only) | Yes (message only) |
|
|
450
|
+
| Privacy tab visible | Yes | Yes | Yes |
|
|
451
|
+
| Messages tab visible | No | No | Yes |
|
|
452
|
+
| Name/email fields | Shown | Hidden | Hidden |
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Security
|
|
457
|
+
|
|
458
|
+
| Credential | Where it lives | Purpose |
|
|
459
|
+
|------------|---------------|---------|
|
|
460
|
+
| **API Key** (`NEXT_PUBLIC_HEEDB_API_KEY`) | Client-side (public) | Identifies your project. Safe to embed in frontend code. |
|
|
461
|
+
| **Widget Secret** (`HEEDB_WIDGET_SECRET`) | Server-side only (private) | Signs the `userHash`. **Never expose in client code, bundles, or git.** |
|
|
462
|
+
|
|
463
|
+
- The API key controls which project receives the submission — it doesn't grant read access to any data
|
|
464
|
+
- The `userHash` is a cryptographic proof that your server vouches for the user's email — without it, users can submit messages but can't read thread history
|
|
465
|
+
- Restrict which domains can use your API key under **Settings > Allowed Domains** in the dashboard
|
|
466
|
+
- If no allowed domains are configured, the widget works from any origin (useful for development)
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Troubleshooting
|
|
471
|
+
|
|
472
|
+
| Problem | Cause | Fix |
|
|
473
|
+
|---------|-------|-----|
|
|
474
|
+
| Widget doesn't appear | Missing `data-api-key` or invalid API key | Check the key in your [dashboard settings](https://heedb.com/app/settings) |
|
|
475
|
+
| "Origin not allowed" (403) | Your domain isn't in the project's allowed domains list | Add your domain in Settings > Allowed Domains, or leave the list empty to allow all |
|
|
476
|
+
| "Failed to load messages" | `userHash` is missing, invalid, or computed with the wrong secret | Verify you're using the Widget Secret (not the API key) and computing HMAC-SHA256 server-side |
|
|
477
|
+
| Widget loads but `init()` has no effect | `init()` called before script loads | With npm: `Heedb.init()` handles this automatically. With CDN: call `init()` in the script's `onload` or after `DOMContentLoaded` |
|
|
478
|
+
| Messages tab not visible | User is identified but not verified | Pass `userHash` from your server to enable the Messages tab |
|
|
479
|
+
|
|
480
|
+
---
|
|
45
481
|
|
|
46
|
-
##
|
|
482
|
+
## License
|
|
47
483
|
|
|
48
|
-
|
|
49
|
-
- [GitHub](https://github.com/TheAleSch/heedb)
|
|
484
|
+
MIT
|
package/dist/widget.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Heedb Widget — https://heedb.com */
|
|
2
|
-
"use strict";(()=>{(function(){var $,q,
|
|
2
|
+
"use strict";(()=>{(function(){var $,D,q,O,R;let y=document.currentScript||document.querySelector("script[data-api-key]"),k=($=y==null?void 0:y.getAttribute("data-api-key"))!=null?$:"",_=(D=y==null?void 0:y.src)!=null?D:"",C=(y==null?void 0:y.getAttribute("data-host"))||(_?new URL(_).origin:"");if(!k){console.warn("[Heedb] Missing data-api-key on <script> tag.");return}let M=!1,b="contact",I="",T=(q=localStorage.getItem("heedb_name"))!=null?q:"",m=(O=localStorage.getItem("heedb_email"))!=null?O:"",f=(R=localStorage.getItem("heedb_token"))!=null?R:"",v=null,P=null;async function F(e,t){try{let o=await S("/api/threads/token",{api_key:k,email:e,userHash:t});if(o.ok){let l=await o.json();if(l.widgetToken){f=l.widgetToken,localStorage.setItem("heedb_token",l.widgetToken);return}}f="",localStorage.removeItem("heedb_token")}catch(o){f="",localStorage.removeItem("heedb_token")}}window.Heedb={init(e={}){e.name&&(T=e.name,localStorage.setItem("heedb_name",e.name)),e.email&&(m=e.email,localStorage.setItem("heedb_email",e.email),e.userHash?F(e.email,e.userHash).then(()=>{B(),M&&v&&h()}):(B(),M&&v&&h()))}};function S(e,t){return fetch(`${C}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}function J(e){let t=new URL(`${C}/api/threads`);return t.searchParams.set("api_key",k),t.searchParams.set("email",e),f&&t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function K(e){let t=new URL(`${C}/api/threads/${e}`);return t.searchParams.set("api_key",k),t.searchParams.set("email",m),t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function U(e,t){return S(`/api/threads/${e}/reply`,{api_key:k,email:m,token:f,message:t})}let W=`
|
|
3
3
|
.llp-btn {
|
|
4
4
|
position: fixed; bottom: 24px; right: 24px; z-index: 999998;
|
|
5
5
|
width: 52px; height: 52px; border-radius: 50%; border: none;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
.llp-btn:hover { transform: scale(1.08); }
|
|
12
12
|
.llp-panel {
|
|
13
13
|
position: fixed; bottom: 88px; right: 24px; z-index: 999999;
|
|
14
|
-
width:
|
|
14
|
+
width: 370px; max-height: 560px; border-radius: 16px;
|
|
15
15
|
background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.18);
|
|
16
16
|
display: flex; flex-direction: column; overflow: hidden;
|
|
17
17
|
font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
.llp-success p { margin: 0; color: #71717a; font-size: 13px; line-height: 1.5; }
|
|
62
62
|
.llp-thread-item {
|
|
63
63
|
padding: 10px 12px; border: 1px solid #e4e4e7; border-radius: 8px;
|
|
64
|
-
margin-bottom: 8px;
|
|
65
|
-
transition: background .1s;
|
|
64
|
+
margin-bottom: 8px; cursor: pointer; display: block; color: inherit;
|
|
65
|
+
transition: background .1s; background: none;
|
|
66
66
|
}
|
|
67
67
|
.llp-thread-item:hover { background: #f4f4f5; }
|
|
68
68
|
.llp-thread-label { font-size: 12px; color: #71717a; margin-bottom: 2px; }
|
|
@@ -77,8 +77,53 @@
|
|
|
77
77
|
border-radius: 8px; font-size: 13px; cursor: pointer; color: #52525b;
|
|
78
78
|
}
|
|
79
79
|
.llp-new-msg button:hover { background: #f4f4f5; }
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
/* Chat view */
|
|
82
|
+
.llp-chat-back {
|
|
83
|
+
background: none; border: none; cursor: pointer; font-size: 13px;
|
|
84
|
+
color: #6366f1; padding: 0; margin-bottom: 12px; font-weight: 500;
|
|
85
|
+
display: flex; align-items: center; gap: 4px;
|
|
86
|
+
}
|
|
87
|
+
.llp-chat-back:hover { text-decoration: underline; }
|
|
88
|
+
.llp-chat-messages {
|
|
89
|
+
display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
|
|
90
|
+
max-height: 320px; overflow-y: auto;
|
|
91
|
+
}
|
|
92
|
+
.llp-chat-bubble {
|
|
93
|
+
padding: 8px 12px; border-radius: 12px; font-size: 13px;
|
|
94
|
+
line-height: 1.45; max-width: 85%; word-wrap: break-word;
|
|
95
|
+
white-space: pre-wrap;
|
|
96
|
+
}
|
|
97
|
+
.llp-chat-inbound {
|
|
98
|
+
background: #f4f4f5; color: #18181b; align-self: flex-start;
|
|
99
|
+
border-bottom-left-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
.llp-chat-outbound {
|
|
102
|
+
background: #18181b; color: #fff; align-self: flex-end;
|
|
103
|
+
border-bottom-right-radius: 4px;
|
|
104
|
+
}
|
|
105
|
+
.llp-chat-time {
|
|
106
|
+
font-size: 10px; color: #a1a1aa; margin-top: 2px;
|
|
107
|
+
}
|
|
108
|
+
.llp-chat-time-right { text-align: right; }
|
|
109
|
+
.llp-chat-reply {
|
|
110
|
+
display: flex; gap: 8px; border-top: 1px solid #e4e4e7; padding-top: 12px;
|
|
111
|
+
}
|
|
112
|
+
.llp-chat-reply-input {
|
|
113
|
+
flex: 1; padding: 8px 10px; border: 1px solid #d4d4d8; border-radius: 8px;
|
|
114
|
+
font-size: 13px; font-family: inherit; color: #18181b;
|
|
115
|
+
resize: none; min-height: 36px; max-height: 80px; box-sizing: border-box;
|
|
116
|
+
}
|
|
117
|
+
.llp-chat-reply-input:focus { outline: none; border-color: #18181b; }
|
|
118
|
+
.llp-chat-send {
|
|
119
|
+
padding: 8px 14px; background: #18181b; color: #fff; border: none;
|
|
120
|
+
border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
121
|
+
transition: opacity .15s; white-space: nowrap; align-self: flex-end;
|
|
122
|
+
}
|
|
123
|
+
.llp-chat-send:hover { opacity: .85; }
|
|
124
|
+
.llp-chat-send:disabled { opacity: .5; cursor: not-allowed; }
|
|
125
|
+
`;function n(e,t={},o=[]){let l=document.createElement(e);for(let[a,s]of Object.entries(t))a==="class"?l.className=s:a==="style"?l.setAttribute("style",s):a.startsWith("on")&&typeof s=="function"?l.addEventListener(a.slice(2),s):l.setAttribute(a,String(s));for(let a of o)l.appendChild(typeof a=="string"?document.createTextNode(a):a);return l}function z(e,t,o=""){let l=document.createElement("input");return l.type=e,l.id=t,l.placeholder=o,l.className="llp-input",l}function V(e,t=""){let o=document.createElement("textarea");return o.id=e,o.placeholder=t,o.className="llp-textarea",o}function Y(e,t){let o=document.createElement("select");o.id=e,o.className="llp-select";for(let l of t){let a=document.createElement("option");a.value=l.value,a.textContent=l.label,o.appendChild(a)}return o}function h(){if(!v)return;let e=v.querySelector(".llp-body");e.innerHTML="",b==="contact"?G(e):b==="privacy"?Q(e):b==="threads"?X(e):b==="thread-detail"&&ee(e),L&&(L.style.display=b==="thread-detail"?"none":"flex")}function G(e,t=""){let o=!!(T&&m),l=n("label",{class:"llp-label"},["Name"]),a=z("text","llp-name","Jane Smith");T&&(a.value=T);let s=n("label",{class:"llp-label"},["Email"]),r=z("email","llp-email","jane@example.com");(t||m)&&(r.value=t||m);let u=n("label",{class:"llp-label"},["Message"]),p=V("llp-message","How can we help?"),c=n("div",{class:"llp-error",style:"display:none"}),i=n("button",{class:"llp-btn-submit"},["Send message"]);i.onclick=async()=>{let d=a.value.trim(),g=r.value.trim(),x=p.value.trim();if(c.style.display="none",!d||!g||!x){c.textContent="Please fill in all fields.",c.style.display="block";return}i.disabled=!0,i.textContent="Sending\u2026";try{let w=await S("/api/contact",{api_key:k,name:d,email:g,message:x});if(w.ok)m=g,localStorage.setItem("heedb_email",g),B(),A(e,"Message sent!","We'll get back to you soon. Check your inbox for a confirmation email.");else{let ne=await w.json();c.textContent=ne.error||"Something went wrong.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}}catch(w){c.textContent="Network error. Please try again.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}},o?e.append(u,p,c,i):e.append(l,a,s,r,u,p,c,i)}function Q(e){let t=!!(T&&m),o=n("label",{class:"llp-label"},["Name"]),l=z("text","llp-priv-name","Jane Smith");T&&(l.value=T);let a=n("label",{class:"llp-label"},["Email"]),s=z("email","llp-priv-email","jane@example.com");m&&(s.value=m);let r=n("label",{class:"llp-label"},["Request type"]),u=Y("llp-priv-type",[{value:"deletion",label:"Delete my data"},{value:"access",label:"Access my data"},{value:"portability",label:"Export my data"},{value:"correction",label:"Correct my data"},{value:"restriction",label:"Restrict processing"},{value:"objection",label:"Object to processing"},{value:"other",label:"Other"}]),p=n("div",{class:"llp-error",style:"display:none"}),c=n("button",{class:"llp-btn-submit"},["Submit request"]);c.onclick=async()=>{let i=l.value.trim(),d=s.value.trim(),g=u.value;if(p.style.display="none",!i||!d){p.textContent="Please fill in all fields.",p.style.display="block";return}c.disabled=!0,c.textContent="Submitting\u2026";try{let x=await S("/api/privacy-request",{api_key:k,name:i,email:d,request_type:g});if(x.ok)m=d,localStorage.setItem("heedb_email",d),B(),A(e,"Request submitted!","Your privacy request has been received. We'll respond within 30 days.");else{let w=await x.json();p.textContent=w.error||"Something went wrong.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}}catch(x){p.textContent="Network error. Please try again.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}},t?e.append(r,u,p,c):e.append(o,l,a,s,r,u,p,c)}async function X(e){var t;if(!f){e.innerHTML="";let o=n("p",{style:"color:#71717a;text-align:center;margin:8px 0;font-size:13px;line-height:1.5"},["Send a message below to view your message history."]),l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.append(o,l);return}e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';try{let{threads:o}=await J(m);if(e.innerHTML="",o.length===0){let s=n("p",{style:"color:#71717a;text-align:center;margin:8px 0"},["No open messages yet."]);e.appendChild(s)}else for(let s of o){let r=n("span",{class:`llp-badge llp-badge-${s.status}`},[s.status]),u=n("div",{class:"llp-thread-label"},[s.type==="privacy"?"Privacy request":"Message"," \u2022 "]);u.appendChild(r);let p=n("div",{class:"llp-thread-text"},[(t=s.preview)!=null?t:""]),c=new Date(s.createdAt).toLocaleDateString(),i=n("div",{class:"llp-thread-meta"},[c]),d=n("div",{class:"llp-thread-item"});d.append(u,p,i),d.onclick=()=>Z(s.id),e.appendChild(d)}let l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a new message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.appendChild(l)}catch(o){e.innerHTML='<p style="color:#ef4444;text-align:center">Failed to load messages.</p>'}}function Z(e){I=e,b="thread-detail",h()}async function ee(e){e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';let t=n("button",{class:"llp-chat-back"},["\u2190 Back to messages"]);t.onclick=()=>{b="threads",E("threads"),h()};try{let l=(await K(I)).messages||[];e.innerHTML="",e.style.padding="12px",e.appendChild(t);let a=n("div",{class:"llp-chat-messages"});for(let i of l){let d=i.direction==="inbound",g=n("div",{style:`display:flex;flex-direction:column;${d?"align-items:flex-start":"align-items:flex-end"}`}),x=n("div",{class:`llp-chat-bubble ${d?"llp-chat-inbound":"llp-chat-outbound"}`},[i.body]),w=n("div",{class:`llp-chat-time ${d?"":"llp-chat-time-right"}`},[te(i.createdAt)]);g.append(x,w),a.appendChild(g)}e.appendChild(a),requestAnimationFrame(()=>{a.scrollTop=a.scrollHeight});let s=n("div",{class:"llp-chat-reply"}),r=document.createElement("textarea");r.className="llp-chat-reply-input",r.placeholder="Type a reply\u2026",r.rows=1,r.addEventListener("input",()=>{r.style.height="auto",r.style.height=Math.min(r.scrollHeight,80)+"px"});let u=n("button",{class:"llp-chat-send"},["Send"]),p=n("div",{class:"llp-error",style:"display:none;margin-top:4px"});async function c(){let i=r.value.trim();if(i){u.disabled=!0,u.textContent="\u2026",p.style.display="none";try{let d=await U(I,i);if(d.ok){let g=n("div",{style:"display:flex;flex-direction:column;align-items:flex-start"}),x=n("div",{class:"llp-chat-bubble llp-chat-inbound"},[i]),w=n("div",{class:"llp-chat-time"},["Just now"]);g.append(x,w),a.appendChild(g),a.scrollTop=a.scrollHeight,r.value="",r.style.height="auto"}else{let g=await d.json();p.textContent=g.error||"Failed to send.",p.style.display="block"}}catch(d){p.textContent="Network error. Please try again.",p.style.display="block"}u.disabled=!1,u.textContent="Send"}}u.onclick=c,r.addEventListener("keydown",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),c())}),s.append(r,u),e.append(s,p),requestAnimationFrame(()=>r.focus())}catch(o){e.innerHTML="",e.appendChild(t),e.appendChild(n("p",{style:"color:#ef4444;text-align:center"},["Failed to load conversation."]))}}function te(e){let t=new Date(e),l=new Date().getTime()-t.getTime(),a=Math.floor(l/6e4);if(a<1)return"Just now";if(a<60)return`${a}m ago`;let s=Math.floor(a/60);return s<24?`${s}h ago`:t.toLocaleDateString()}function A(e,t,o){e.innerHTML="";let l=n("div",{class:"llp-success"});l.innerHTML=`
|
|
81
126
|
<div class="llp-success-icon">\u2705</div>
|
|
82
|
-
<h3>${
|
|
83
|
-
<p>${
|
|
84
|
-
`;let
|
|
127
|
+
<h3>${t}</h3>
|
|
128
|
+
<p>${o}</p>
|
|
129
|
+
`;let a=n("a",{href:`${C}/app?email=${encodeURIComponent(m)}`,target:"_blank",style:"display:inline-block;margin-top:12px;color:#18181b;font-size:13px;font-weight:600"},["View your messages \u2192"]);l.appendChild(a),e.appendChild(l)}let L=null,H=null;function E(e){b=e,L&&L.querySelectorAll(".llp-tab").forEach(t=>{t.classList.toggle("active",t.dataset.mode===e)})}function B(){H&&(H.style.display=m&&f?"":"none")}function j(){let e=document.createElement("style");e.textContent=W,document.head.appendChild(e),P=n("button",{class:"llp-btn",title:"Contact us","aria-label":"Open contact widget"},["\u{1F4AC}"]),P.onclick=N,document.body.appendChild(P),v=n("div",{class:"llp-panel",style:"display:none"});let t=n("div",{class:"llp-header"}),o=n("h2",{},["Contact us"]),l=n("button",{class:"llp-close","aria-label":"Close"},["\u2715"]);l.onclick=N,t.append(o,l),L=n("div",{class:"llp-tabs"});let a=n("button",{class:"llp-tab active","data-mode":"contact"},["Message"]),s=n("button",{class:"llp-tab","data-mode":"privacy"},["Privacy"]);H=n("button",{class:"llp-tab","data-mode":"threads"},["Messages"]),H.style.display=m&&f?"":"none",a.onclick=()=>{E("contact"),h()},s.onclick=()=>{E("privacy"),h()},H.onclick=()=>{E("threads"),h()},L.append(a,s,H);let r=n("div",{class:"llp-body"});v.append(t,L,r),document.body.appendChild(v)}function N(){M=!M,v&&(v.style.display=M?"flex":"none",M&&(m&&f&&b!=="privacy"&&b!=="thread-detail"&&(b="threads",E("threads")),h()))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",j):j()})();})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heedb/web-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Drop-in feedback widget with email conversations for any website",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
+
"widget.js",
|
|
27
28
|
"README.md"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
package/widget.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Heedb Widget — https://heedb.com */
|
|
2
|
+
"use strict";(()=>{(function(){var $,D,q,O,R;let y=document.currentScript||document.querySelector("script[data-api-key]"),k=($=y==null?void 0:y.getAttribute("data-api-key"))!=null?$:"",_=(D=y==null?void 0:y.src)!=null?D:"",C=(y==null?void 0:y.getAttribute("data-host"))||(_?new URL(_).origin:"");if(!k){console.warn("[Heedb] Missing data-api-key on <script> tag.");return}let M=!1,b="contact",I="",T=(q=localStorage.getItem("heedb_name"))!=null?q:"",m=(O=localStorage.getItem("heedb_email"))!=null?O:"",f=(R=localStorage.getItem("heedb_token"))!=null?R:"",v=null,P=null;async function F(e,t){try{let o=await S("/api/threads/token",{api_key:k,email:e,userHash:t});if(o.ok){let l=await o.json();if(l.widgetToken){f=l.widgetToken,localStorage.setItem("heedb_token",l.widgetToken);return}}f="",localStorage.removeItem("heedb_token")}catch(o){f="",localStorage.removeItem("heedb_token")}}window.Heedb={init(e={}){e.name&&(T=e.name,localStorage.setItem("heedb_name",e.name)),e.email&&(m=e.email,localStorage.setItem("heedb_email",e.email),e.userHash?F(e.email,e.userHash).then(()=>{B(),M&&v&&h()}):(B(),M&&v&&h()))}};function S(e,t){return fetch(`${C}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}function J(e){let t=new URL(`${C}/api/threads`);return t.searchParams.set("api_key",k),t.searchParams.set("email",e),f&&t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function K(e){let t=new URL(`${C}/api/threads/${e}`);return t.searchParams.set("api_key",k),t.searchParams.set("email",m),t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function U(e,t){return S(`/api/threads/${e}/reply`,{api_key:k,email:m,token:f,message:t})}let W=`
|
|
3
|
+
.llp-btn {
|
|
4
|
+
position: fixed; bottom: 24px; right: 24px; z-index: 999998;
|
|
5
|
+
width: 52px; height: 52px; border-radius: 50%; border: none;
|
|
6
|
+
background: #18181b; color: #fff; font-size: 22px; cursor: pointer;
|
|
7
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.25);
|
|
8
|
+
display: flex; align-items: center; justify-content: center;
|
|
9
|
+
transition: transform .15s;
|
|
10
|
+
}
|
|
11
|
+
.llp-btn:hover { transform: scale(1.08); }
|
|
12
|
+
.llp-panel {
|
|
13
|
+
position: fixed; bottom: 88px; right: 24px; z-index: 999999;
|
|
14
|
+
width: 370px; max-height: 560px; border-radius: 16px;
|
|
15
|
+
background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.18);
|
|
16
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
17
|
+
font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
|
|
18
|
+
transition: opacity .15s, transform .15s;
|
|
19
|
+
}
|
|
20
|
+
.llp-header {
|
|
21
|
+
background: #18181b; color: #fff; padding: 14px 16px;
|
|
22
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
23
|
+
}
|
|
24
|
+
.llp-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
25
|
+
.llp-close {
|
|
26
|
+
background: none; border: none; color: #fff; cursor: pointer;
|
|
27
|
+
font-size: 18px; line-height: 1; padding: 0;
|
|
28
|
+
}
|
|
29
|
+
.llp-tabs {
|
|
30
|
+
display: flex; border-bottom: 1px solid #e4e4e7;
|
|
31
|
+
}
|
|
32
|
+
.llp-tab {
|
|
33
|
+
flex: 1; padding: 10px 0; border: none; background: none;
|
|
34
|
+
cursor: pointer; font-size: 13px; font-weight: 500; color: #71717a;
|
|
35
|
+
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
36
|
+
}
|
|
37
|
+
.llp-tab.active { color: #18181b; border-bottom-color: #18181b; }
|
|
38
|
+
.llp-body { padding: 16px; overflow-y: auto; flex: 1; }
|
|
39
|
+
.llp-label { display: block; font-size: 12px; font-weight: 600; color: #52525b; margin-bottom: 4px; }
|
|
40
|
+
.llp-input, .llp-textarea, .llp-select {
|
|
41
|
+
width: 100%; box-sizing: border-box; padding: 8px 10px;
|
|
42
|
+
border: 1px solid #d4d4d8; border-radius: 8px;
|
|
43
|
+
font-size: 13px; color: #18181b; margin-bottom: 10px;
|
|
44
|
+
font-family: inherit; background: #fff;
|
|
45
|
+
}
|
|
46
|
+
.llp-textarea { min-height: 80px; resize: vertical; }
|
|
47
|
+
.llp-input:focus, .llp-textarea:focus, .llp-select:focus {
|
|
48
|
+
outline: none; border-color: #18181b;
|
|
49
|
+
}
|
|
50
|
+
.llp-btn-submit {
|
|
51
|
+
width: 100%; padding: 10px; background: #18181b; color: #fff;
|
|
52
|
+
border: none; border-radius: 8px; font-size: 14px; font-weight: 600;
|
|
53
|
+
cursor: pointer; transition: opacity .15s;
|
|
54
|
+
}
|
|
55
|
+
.llp-btn-submit:hover { opacity: .85; }
|
|
56
|
+
.llp-btn-submit:disabled { opacity: .5; cursor: not-allowed; }
|
|
57
|
+
.llp-error { color: #ef4444; font-size: 12px; margin-bottom: 8px; }
|
|
58
|
+
.llp-success { text-align: center; padding: 24px 8px; }
|
|
59
|
+
.llp-success-icon { font-size: 36px; }
|
|
60
|
+
.llp-success h3 { margin: 12px 0 6px; font-size: 15px; color: #18181b; }
|
|
61
|
+
.llp-success p { margin: 0; color: #71717a; font-size: 13px; line-height: 1.5; }
|
|
62
|
+
.llp-thread-item {
|
|
63
|
+
padding: 10px 12px; border: 1px solid #e4e4e7; border-radius: 8px;
|
|
64
|
+
margin-bottom: 8px; cursor: pointer; display: block; color: inherit;
|
|
65
|
+
transition: background .1s; background: none;
|
|
66
|
+
}
|
|
67
|
+
.llp-thread-item:hover { background: #f4f4f5; }
|
|
68
|
+
.llp-thread-label { font-size: 12px; color: #71717a; margin-bottom: 2px; }
|
|
69
|
+
.llp-thread-text { font-size: 13px; color: #18181b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
70
|
+
.llp-thread-meta { font-size: 11px; color: #a1a1aa; margin-top: 3px; }
|
|
71
|
+
.llp-badge { display: inline-block; padding: 1px 6px; border-radius: 9999px; font-size: 10px; font-weight: 600; }
|
|
72
|
+
.llp-badge-open { background: #d1fae5; color: #065f46; }
|
|
73
|
+
.llp-badge-replied { background: #dbeafe; color: #1e40af; }
|
|
74
|
+
.llp-new-msg { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e4e4e7; }
|
|
75
|
+
.llp-new-msg button {
|
|
76
|
+
width: 100%; padding: 8px; background: none; border: 1px solid #d4d4d8;
|
|
77
|
+
border-radius: 8px; font-size: 13px; cursor: pointer; color: #52525b;
|
|
78
|
+
}
|
|
79
|
+
.llp-new-msg button:hover { background: #f4f4f5; }
|
|
80
|
+
|
|
81
|
+
/* Chat view */
|
|
82
|
+
.llp-chat-back {
|
|
83
|
+
background: none; border: none; cursor: pointer; font-size: 13px;
|
|
84
|
+
color: #6366f1; padding: 0; margin-bottom: 12px; font-weight: 500;
|
|
85
|
+
display: flex; align-items: center; gap: 4px;
|
|
86
|
+
}
|
|
87
|
+
.llp-chat-back:hover { text-decoration: underline; }
|
|
88
|
+
.llp-chat-messages {
|
|
89
|
+
display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
|
|
90
|
+
max-height: 320px; overflow-y: auto;
|
|
91
|
+
}
|
|
92
|
+
.llp-chat-bubble {
|
|
93
|
+
padding: 8px 12px; border-radius: 12px; font-size: 13px;
|
|
94
|
+
line-height: 1.45; max-width: 85%; word-wrap: break-word;
|
|
95
|
+
white-space: pre-wrap;
|
|
96
|
+
}
|
|
97
|
+
.llp-chat-inbound {
|
|
98
|
+
background: #f4f4f5; color: #18181b; align-self: flex-start;
|
|
99
|
+
border-bottom-left-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
.llp-chat-outbound {
|
|
102
|
+
background: #18181b; color: #fff; align-self: flex-end;
|
|
103
|
+
border-bottom-right-radius: 4px;
|
|
104
|
+
}
|
|
105
|
+
.llp-chat-time {
|
|
106
|
+
font-size: 10px; color: #a1a1aa; margin-top: 2px;
|
|
107
|
+
}
|
|
108
|
+
.llp-chat-time-right { text-align: right; }
|
|
109
|
+
.llp-chat-reply {
|
|
110
|
+
display: flex; gap: 8px; border-top: 1px solid #e4e4e7; padding-top: 12px;
|
|
111
|
+
}
|
|
112
|
+
.llp-chat-reply-input {
|
|
113
|
+
flex: 1; padding: 8px 10px; border: 1px solid #d4d4d8; border-radius: 8px;
|
|
114
|
+
font-size: 13px; font-family: inherit; color: #18181b;
|
|
115
|
+
resize: none; min-height: 36px; max-height: 80px; box-sizing: border-box;
|
|
116
|
+
}
|
|
117
|
+
.llp-chat-reply-input:focus { outline: none; border-color: #18181b; }
|
|
118
|
+
.llp-chat-send {
|
|
119
|
+
padding: 8px 14px; background: #18181b; color: #fff; border: none;
|
|
120
|
+
border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
121
|
+
transition: opacity .15s; white-space: nowrap; align-self: flex-end;
|
|
122
|
+
}
|
|
123
|
+
.llp-chat-send:hover { opacity: .85; }
|
|
124
|
+
.llp-chat-send:disabled { opacity: .5; cursor: not-allowed; }
|
|
125
|
+
`;function n(e,t={},o=[]){let l=document.createElement(e);for(let[a,s]of Object.entries(t))a==="class"?l.className=s:a==="style"?l.setAttribute("style",s):a.startsWith("on")&&typeof s=="function"?l.addEventListener(a.slice(2),s):l.setAttribute(a,String(s));for(let a of o)l.appendChild(typeof a=="string"?document.createTextNode(a):a);return l}function z(e,t,o=""){let l=document.createElement("input");return l.type=e,l.id=t,l.placeholder=o,l.className="llp-input",l}function V(e,t=""){let o=document.createElement("textarea");return o.id=e,o.placeholder=t,o.className="llp-textarea",o}function Y(e,t){let o=document.createElement("select");o.id=e,o.className="llp-select";for(let l of t){let a=document.createElement("option");a.value=l.value,a.textContent=l.label,o.appendChild(a)}return o}function h(){if(!v)return;let e=v.querySelector(".llp-body");e.innerHTML="",b==="contact"?G(e):b==="privacy"?Q(e):b==="threads"?X(e):b==="thread-detail"&&ee(e),L&&(L.style.display=b==="thread-detail"?"none":"flex")}function G(e,t=""){let o=!!(T&&m),l=n("label",{class:"llp-label"},["Name"]),a=z("text","llp-name","Jane Smith");T&&(a.value=T);let s=n("label",{class:"llp-label"},["Email"]),r=z("email","llp-email","jane@example.com");(t||m)&&(r.value=t||m);let u=n("label",{class:"llp-label"},["Message"]),p=V("llp-message","How can we help?"),c=n("div",{class:"llp-error",style:"display:none"}),i=n("button",{class:"llp-btn-submit"},["Send message"]);i.onclick=async()=>{let d=a.value.trim(),g=r.value.trim(),x=p.value.trim();if(c.style.display="none",!d||!g||!x){c.textContent="Please fill in all fields.",c.style.display="block";return}i.disabled=!0,i.textContent="Sending\u2026";try{let w=await S("/api/contact",{api_key:k,name:d,email:g,message:x});if(w.ok)m=g,localStorage.setItem("heedb_email",g),B(),A(e,"Message sent!","We'll get back to you soon. Check your inbox for a confirmation email.");else{let ne=await w.json();c.textContent=ne.error||"Something went wrong.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}}catch(w){c.textContent="Network error. Please try again.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}},o?e.append(u,p,c,i):e.append(l,a,s,r,u,p,c,i)}function Q(e){let t=!!(T&&m),o=n("label",{class:"llp-label"},["Name"]),l=z("text","llp-priv-name","Jane Smith");T&&(l.value=T);let a=n("label",{class:"llp-label"},["Email"]),s=z("email","llp-priv-email","jane@example.com");m&&(s.value=m);let r=n("label",{class:"llp-label"},["Request type"]),u=Y("llp-priv-type",[{value:"deletion",label:"Delete my data"},{value:"access",label:"Access my data"},{value:"portability",label:"Export my data"},{value:"correction",label:"Correct my data"},{value:"restriction",label:"Restrict processing"},{value:"objection",label:"Object to processing"},{value:"other",label:"Other"}]),p=n("div",{class:"llp-error",style:"display:none"}),c=n("button",{class:"llp-btn-submit"},["Submit request"]);c.onclick=async()=>{let i=l.value.trim(),d=s.value.trim(),g=u.value;if(p.style.display="none",!i||!d){p.textContent="Please fill in all fields.",p.style.display="block";return}c.disabled=!0,c.textContent="Submitting\u2026";try{let x=await S("/api/privacy-request",{api_key:k,name:i,email:d,request_type:g});if(x.ok)m=d,localStorage.setItem("heedb_email",d),B(),A(e,"Request submitted!","Your privacy request has been received. We'll respond within 30 days.");else{let w=await x.json();p.textContent=w.error||"Something went wrong.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}}catch(x){p.textContent="Network error. Please try again.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}},t?e.append(r,u,p,c):e.append(o,l,a,s,r,u,p,c)}async function X(e){var t;if(!f){e.innerHTML="";let o=n("p",{style:"color:#71717a;text-align:center;margin:8px 0;font-size:13px;line-height:1.5"},["Send a message below to view your message history."]),l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.append(o,l);return}e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';try{let{threads:o}=await J(m);if(e.innerHTML="",o.length===0){let s=n("p",{style:"color:#71717a;text-align:center;margin:8px 0"},["No open messages yet."]);e.appendChild(s)}else for(let s of o){let r=n("span",{class:`llp-badge llp-badge-${s.status}`},[s.status]),u=n("div",{class:"llp-thread-label"},[s.type==="privacy"?"Privacy request":"Message"," \u2022 "]);u.appendChild(r);let p=n("div",{class:"llp-thread-text"},[(t=s.preview)!=null?t:""]),c=new Date(s.createdAt).toLocaleDateString(),i=n("div",{class:"llp-thread-meta"},[c]),d=n("div",{class:"llp-thread-item"});d.append(u,p,i),d.onclick=()=>Z(s.id),e.appendChild(d)}let l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a new message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.appendChild(l)}catch(o){e.innerHTML='<p style="color:#ef4444;text-align:center">Failed to load messages.</p>'}}function Z(e){I=e,b="thread-detail",h()}async function ee(e){e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';let t=n("button",{class:"llp-chat-back"},["\u2190 Back to messages"]);t.onclick=()=>{b="threads",E("threads"),h()};try{let l=(await K(I)).messages||[];e.innerHTML="",e.style.padding="12px",e.appendChild(t);let a=n("div",{class:"llp-chat-messages"});for(let i of l){let d=i.direction==="inbound",g=n("div",{style:`display:flex;flex-direction:column;${d?"align-items:flex-start":"align-items:flex-end"}`}),x=n("div",{class:`llp-chat-bubble ${d?"llp-chat-inbound":"llp-chat-outbound"}`},[i.body]),w=n("div",{class:`llp-chat-time ${d?"":"llp-chat-time-right"}`},[te(i.createdAt)]);g.append(x,w),a.appendChild(g)}e.appendChild(a),requestAnimationFrame(()=>{a.scrollTop=a.scrollHeight});let s=n("div",{class:"llp-chat-reply"}),r=document.createElement("textarea");r.className="llp-chat-reply-input",r.placeholder="Type a reply\u2026",r.rows=1,r.addEventListener("input",()=>{r.style.height="auto",r.style.height=Math.min(r.scrollHeight,80)+"px"});let u=n("button",{class:"llp-chat-send"},["Send"]),p=n("div",{class:"llp-error",style:"display:none;margin-top:4px"});async function c(){let i=r.value.trim();if(i){u.disabled=!0,u.textContent="\u2026",p.style.display="none";try{let d=await U(I,i);if(d.ok){let g=n("div",{style:"display:flex;flex-direction:column;align-items:flex-start"}),x=n("div",{class:"llp-chat-bubble llp-chat-inbound"},[i]),w=n("div",{class:"llp-chat-time"},["Just now"]);g.append(x,w),a.appendChild(g),a.scrollTop=a.scrollHeight,r.value="",r.style.height="auto"}else{let g=await d.json();p.textContent=g.error||"Failed to send.",p.style.display="block"}}catch(d){p.textContent="Network error. Please try again.",p.style.display="block"}u.disabled=!1,u.textContent="Send"}}u.onclick=c,r.addEventListener("keydown",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),c())}),s.append(r,u),e.append(s,p),requestAnimationFrame(()=>r.focus())}catch(o){e.innerHTML="",e.appendChild(t),e.appendChild(n("p",{style:"color:#ef4444;text-align:center"},["Failed to load conversation."]))}}function te(e){let t=new Date(e),l=new Date().getTime()-t.getTime(),a=Math.floor(l/6e4);if(a<1)return"Just now";if(a<60)return`${a}m ago`;let s=Math.floor(a/60);return s<24?`${s}h ago`:t.toLocaleDateString()}function A(e,t,o){e.innerHTML="";let l=n("div",{class:"llp-success"});l.innerHTML=`
|
|
126
|
+
<div class="llp-success-icon">\u2705</div>
|
|
127
|
+
<h3>${t}</h3>
|
|
128
|
+
<p>${o}</p>
|
|
129
|
+
`;let a=n("a",{href:`${C}/app?email=${encodeURIComponent(m)}`,target:"_blank",style:"display:inline-block;margin-top:12px;color:#18181b;font-size:13px;font-weight:600"},["View your messages \u2192"]);l.appendChild(a),e.appendChild(l)}let L=null,H=null;function E(e){b=e,L&&L.querySelectorAll(".llp-tab").forEach(t=>{t.classList.toggle("active",t.dataset.mode===e)})}function B(){H&&(H.style.display=m&&f?"":"none")}function j(){let e=document.createElement("style");e.textContent=W,document.head.appendChild(e),P=n("button",{class:"llp-btn",title:"Contact us","aria-label":"Open contact widget"},["\u{1F4AC}"]),P.onclick=N,document.body.appendChild(P),v=n("div",{class:"llp-panel",style:"display:none"});let t=n("div",{class:"llp-header"}),o=n("h2",{},["Contact us"]),l=n("button",{class:"llp-close","aria-label":"Close"},["\u2715"]);l.onclick=N,t.append(o,l),L=n("div",{class:"llp-tabs"});let a=n("button",{class:"llp-tab active","data-mode":"contact"},["Message"]),s=n("button",{class:"llp-tab","data-mode":"privacy"},["Privacy"]);H=n("button",{class:"llp-tab","data-mode":"threads"},["Messages"]),H.style.display=m&&f?"":"none",a.onclick=()=>{E("contact"),h()},s.onclick=()=>{E("privacy"),h()},H.onclick=()=>{E("threads"),h()},L.append(a,s,H);let r=n("div",{class:"llp-body"});v.append(t,L,r),document.body.appendChild(v)}function N(){M=!M,v&&(v.style.display=M?"flex":"none",M&&(m&&f&&b!=="privacy"&&b!=="thread-detail"&&(b="threads",E("threads")),h()))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",j):j()})();})();
|