@cyguin/feedback 0.1.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/.github/workflows/publish.yml +27 -0
- package/LICENSE +7 -0
- package/README.md +136 -0
- package/dist/adapters/in-memory.cjs +6 -0
- package/dist/adapters/in-memory.d.cts +23 -0
- package/dist/adapters/in-memory.d.ts +23 -0
- package/dist/adapters/in-memory.js +6 -0
- package/dist/adapters/supabase.cjs +6 -0
- package/dist/adapters/supabase.d.cts +27 -0
- package/dist/adapters/supabase.d.ts +27 -0
- package/dist/adapters/supabase.js +6 -0
- package/dist/chunk-3CF2TRVK.js +202 -0
- package/dist/chunk-5Q4LNXS7.cjs +46 -0
- package/dist/chunk-CEOUAA46.cjs +202 -0
- package/dist/chunk-F4UO2IK4.cjs +74 -0
- package/dist/chunk-IZIGPLKR.js +74 -0
- package/dist/chunk-KXORPGTI.js +57 -0
- package/dist/chunk-NKCO24NR.js +46 -0
- package/dist/chunk-SRH4NGAR.cjs +57 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +18 -0
- package/dist/react.cjs +6 -0
- package/dist/react.d.cts +7 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +6 -0
- package/dist/server.cjs +6 -0
- package/dist/server.d.cts +20 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +6 -0
- package/dist/types-CMsn8uhD.d.cts +49 -0
- package/dist/types-CMsn8uhD.d.ts +49 -0
- package/package.json +66 -0
- package/src/adapters/in-memory.ts +58 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/supabase.ts +88 -0
- package/src/app/api/feedback/[...cyguin]/route.ts +20 -0
- package/src/components/FeedbackWidget.tsx +215 -0
- package/src/components/index.ts +2 -0
- package/src/handlers/route.ts +100 -0
- package/src/index.ts +11 -0
- package/src/server.ts +1 -0
- package/src/types.ts +50 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '20'
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: npm install --legacy-peer-deps
|
|
22
|
+
- name: Build
|
|
23
|
+
run: npm run build
|
|
24
|
+
- name: Publish
|
|
25
|
+
env:
|
|
26
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
27
|
+
run: npm publish --provenance --access public
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 cyguin.com
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @cyguin/feedback
|
|
2
|
+
|
|
3
|
+
Drop-in user feedback widget for Next.js — capture thumbs, star ratings, or free-text feedback from inside any page.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cyguin/feedback
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Create the API route
|
|
14
|
+
|
|
15
|
+
Create `app/api/feedback/[...cyguin]/route.ts` in your Next.js app:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createFeedbackHandler } from '@cyguin/feedback/server';
|
|
19
|
+
import { createInMemoryAdapter } from '@cyguin/feedback/adapters/in-memory';
|
|
20
|
+
|
|
21
|
+
const adapter = createInMemoryAdapter();
|
|
22
|
+
const handler = createFeedbackHandler({ adapter });
|
|
23
|
+
|
|
24
|
+
export { handler as GET, handler as POST, handler as PATCH };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For production with Supabase:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createFeedbackHandler } from '@cyguin/feedback/server';
|
|
31
|
+
import { createSupabaseAdapter } from '@cyguin/feedback/adapters/supabase';
|
|
32
|
+
|
|
33
|
+
const adapter = createSupabaseAdapter(supabaseClient);
|
|
34
|
+
const handler = createFeedbackHandler({ adapter });
|
|
35
|
+
|
|
36
|
+
export { handler as GET, handler as POST, handler as PATCH };
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Run migrations
|
|
40
|
+
|
|
41
|
+
```sql
|
|
42
|
+
CREATE TABLE feedback (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
user_id TEXT,
|
|
45
|
+
type TEXT NOT NULL,
|
|
46
|
+
body TEXT NOT NULL,
|
|
47
|
+
url TEXT NOT NULL,
|
|
48
|
+
created_at INTEGER NOT NULL,
|
|
49
|
+
reviewed INTEGER NOT NULL DEFAULT 0
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Add the widget
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { FeedbackWidget } from '@cyguin/feedback/react';
|
|
57
|
+
|
|
58
|
+
// Thumbs mode
|
|
59
|
+
<FeedbackWidget type="thumbs" />
|
|
60
|
+
|
|
61
|
+
// Star rating mode (default 5 stars)
|
|
62
|
+
<FeedbackWidget type="rating" maxStars={5} />
|
|
63
|
+
|
|
64
|
+
// Free text mode
|
|
65
|
+
<FeedbackWidget type="text" placeholder="Share your feedback..." />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
All three modes submit to your API and show a "Thanks for your feedback!" message for 3 seconds.
|
|
69
|
+
|
|
70
|
+
## Theming
|
|
71
|
+
|
|
72
|
+
Use `--cyguin-*` CSS custom properties on a parent element or `:root`:
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
.feedback-widget {
|
|
76
|
+
--cyguin-bg: #ffffff;
|
|
77
|
+
--cyguin-bg-subtle: #f5f5f5;
|
|
78
|
+
--cyguin-border: #e5e5e5;
|
|
79
|
+
--cyguin-border-focus: #f5a800;
|
|
80
|
+
--cyguin-fg: #0a0a0a;
|
|
81
|
+
--cyguin-fg-muted: #888888;
|
|
82
|
+
--cyguin-accent: #f5a800;
|
|
83
|
+
--cyguin-accent-dark: #c47f00;
|
|
84
|
+
--cyguin-accent-fg: #0a0a0a;
|
|
85
|
+
--cyguin-radius: 6px;
|
|
86
|
+
--cyguin-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Switch to dark theme with the `theme` prop:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<FeedbackWidget type="thumbs" theme="dark" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Dark theme applies these overrides automatically:
|
|
97
|
+
|
|
98
|
+
```css
|
|
99
|
+
--cyguin-bg: #0a0a0a;
|
|
100
|
+
--cyguin-bg-subtle: #1a1a1a;
|
|
101
|
+
--cyguin-border: #2a2a2a;
|
|
102
|
+
--cyguin-fg: #f5f5f5;
|
|
103
|
+
--cyguin-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Supabase Adapter Setup
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { createFeedbackHandler } from '@cyguin/feedback/server';
|
|
110
|
+
import { createSupabaseAdapter } from '@cyguin/feedback/adapters/supabase';
|
|
111
|
+
import { createClient } from '@supabase/supabase-js';
|
|
112
|
+
|
|
113
|
+
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
|
|
114
|
+
const adapter = createSupabaseAdapter(supabase);
|
|
115
|
+
|
|
116
|
+
const handler = createFeedbackHandler({
|
|
117
|
+
adapter,
|
|
118
|
+
secret: process.env.FEEDBACK_SECRET,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export { handler as GET, handler as POST, handler as PATCH };
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Required environment variable: `FEEDBACK_SECRET` — Bearer token for admin API access.
|
|
125
|
+
|
|
126
|
+
## API Routes
|
|
127
|
+
|
|
128
|
+
| Method | Route | Description |
|
|
129
|
+
|--------|-------|-------------|
|
|
130
|
+
| GET | `/api/feedback` | List feedback (admin, requires Bearer token) |
|
|
131
|
+
| POST | `/api/feedback` | Submit new feedback |
|
|
132
|
+
| PATCH | `/api/feedback/:id/reviewed` | Toggle reviewed flag |
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { F as FeedbackAdapter, b as FeedbackRecord, c as FeedbackType } from '../types-CMsn8uhD.cjs';
|
|
2
|
+
|
|
3
|
+
declare class InMemoryFeedbackAdapter implements FeedbackAdapter {
|
|
4
|
+
private store;
|
|
5
|
+
list(opts?: {
|
|
6
|
+
url?: string;
|
|
7
|
+
reviewed?: boolean;
|
|
8
|
+
limit?: number;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
data: FeedbackRecord[];
|
|
11
|
+
total: number;
|
|
12
|
+
}>;
|
|
13
|
+
create(record: {
|
|
14
|
+
userId?: string;
|
|
15
|
+
type: FeedbackType;
|
|
16
|
+
body: string;
|
|
17
|
+
url: string;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
}): Promise<FeedbackRecord>;
|
|
20
|
+
setReviewed(id: string, reviewed: boolean): Promise<FeedbackRecord>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { InMemoryFeedbackAdapter };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { F as FeedbackAdapter, b as FeedbackRecord, c as FeedbackType } from '../types-CMsn8uhD.js';
|
|
2
|
+
|
|
3
|
+
declare class InMemoryFeedbackAdapter implements FeedbackAdapter {
|
|
4
|
+
private store;
|
|
5
|
+
list(opts?: {
|
|
6
|
+
url?: string;
|
|
7
|
+
reviewed?: boolean;
|
|
8
|
+
limit?: number;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
data: FeedbackRecord[];
|
|
11
|
+
total: number;
|
|
12
|
+
}>;
|
|
13
|
+
create(record: {
|
|
14
|
+
userId?: string;
|
|
15
|
+
type: FeedbackType;
|
|
16
|
+
body: string;
|
|
17
|
+
url: string;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
}): Promise<FeedbackRecord>;
|
|
20
|
+
setReviewed(id: string, reviewed: boolean): Promise<FeedbackRecord>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { InMemoryFeedbackAdapter };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { F as FeedbackAdapter, b as FeedbackRecord, c as FeedbackType } from '../types-CMsn8uhD.cjs';
|
|
2
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
declare class SupabaseFeedbackAdapter implements FeedbackAdapter {
|
|
5
|
+
private client;
|
|
6
|
+
private tableName;
|
|
7
|
+
constructor(client: SupabaseClient, tableName?: string);
|
|
8
|
+
private mapRow;
|
|
9
|
+
list(opts?: {
|
|
10
|
+
url?: string;
|
|
11
|
+
reviewed?: boolean;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
data: FeedbackRecord[];
|
|
15
|
+
total: number;
|
|
16
|
+
}>;
|
|
17
|
+
create(record: {
|
|
18
|
+
userId?: string;
|
|
19
|
+
type: FeedbackType;
|
|
20
|
+
body: string;
|
|
21
|
+
url: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}): Promise<FeedbackRecord>;
|
|
24
|
+
setReviewed(id: string, reviewed: boolean): Promise<FeedbackRecord>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { SupabaseFeedbackAdapter };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { F as FeedbackAdapter, b as FeedbackRecord, c as FeedbackType } from '../types-CMsn8uhD.js';
|
|
2
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
declare class SupabaseFeedbackAdapter implements FeedbackAdapter {
|
|
5
|
+
private client;
|
|
6
|
+
private tableName;
|
|
7
|
+
constructor(client: SupabaseClient, tableName?: string);
|
|
8
|
+
private mapRow;
|
|
9
|
+
list(opts?: {
|
|
10
|
+
url?: string;
|
|
11
|
+
reviewed?: boolean;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
data: FeedbackRecord[];
|
|
15
|
+
total: number;
|
|
16
|
+
}>;
|
|
17
|
+
create(record: {
|
|
18
|
+
userId?: string;
|
|
19
|
+
type: FeedbackType;
|
|
20
|
+
body: string;
|
|
21
|
+
url: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}): Promise<FeedbackRecord>;
|
|
24
|
+
setReviewed(id: string, reviewed: boolean): Promise<FeedbackRecord>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { SupabaseFeedbackAdapter };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/components/FeedbackWidget.tsx
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var THUMBS_UP = "thumbs_up";
|
|
5
|
+
var THUMBS_DOWN = "thumbs_down";
|
|
6
|
+
function ThumbsUpIcon() {
|
|
7
|
+
return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
8
|
+
/* @__PURE__ */ jsx("path", { d: "M7 10v12" }),
|
|
9
|
+
/* @__PURE__ */ jsx("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" })
|
|
10
|
+
] });
|
|
11
|
+
}
|
|
12
|
+
function ThumbsDownIcon() {
|
|
13
|
+
return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
14
|
+
/* @__PURE__ */ jsx("path", { d: "M17 14V2" }),
|
|
15
|
+
/* @__PURE__ */ jsx("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" })
|
|
16
|
+
] });
|
|
17
|
+
}
|
|
18
|
+
function StarIcon({ filled }) {
|
|
19
|
+
return /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: filled ? "currentColor" : "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" }) });
|
|
20
|
+
}
|
|
21
|
+
var lightTheme = {
|
|
22
|
+
"--cyguin-bg": "#ffffff",
|
|
23
|
+
"--cyguin-bg-subtle": "#f5f5f5",
|
|
24
|
+
"--cyguin-border": "#e5e5e5",
|
|
25
|
+
"--cyguin-border-focus": "#f5a800",
|
|
26
|
+
"--cyguin-fg": "#0a0a0a",
|
|
27
|
+
"--cyguin-fg-muted": "#888888",
|
|
28
|
+
"--cyguin-accent": "#f5a800",
|
|
29
|
+
"--cyguin-accent-dark": "#c47f00",
|
|
30
|
+
"--cyguin-accent-fg": "#0a0a0a",
|
|
31
|
+
"--cyguin-radius": "6px",
|
|
32
|
+
"--cyguin-shadow": "0 1px 4px rgba(0,0,0,0.08)"
|
|
33
|
+
};
|
|
34
|
+
var darkTheme = {
|
|
35
|
+
"--cyguin-bg": "#0a0a0a",
|
|
36
|
+
"--cyguin-bg-subtle": "#1a1a1a",
|
|
37
|
+
"--cyguin-border": "#2a2a2a",
|
|
38
|
+
"--cyguin-border-focus": "#f5a800",
|
|
39
|
+
"--cyguin-fg": "#f5f5f5",
|
|
40
|
+
"--cyguin-fg-muted": "#888888",
|
|
41
|
+
"--cyguin-accent": "#f5a800",
|
|
42
|
+
"--cyguin-accent-dark": "#c47f00",
|
|
43
|
+
"--cyguin-accent-fg": "#0a0a0a",
|
|
44
|
+
"--cyguin-radius": "6px",
|
|
45
|
+
"--cyguin-shadow": "0 1px 4px rgba(0,0,0,0.4)"
|
|
46
|
+
};
|
|
47
|
+
function FeedbackWidget({
|
|
48
|
+
type,
|
|
49
|
+
url: urlProp,
|
|
50
|
+
userId,
|
|
51
|
+
theme = "light",
|
|
52
|
+
className = "",
|
|
53
|
+
onSubmit,
|
|
54
|
+
maxStars = 5,
|
|
55
|
+
placeholder = "Share your feedback..."
|
|
56
|
+
}) {
|
|
57
|
+
const [submitted, setSubmitted] = useState(false);
|
|
58
|
+
const [textValue, setTextValue] = useState("");
|
|
59
|
+
const [loading, setLoading] = useState(false);
|
|
60
|
+
const getUrl = () => {
|
|
61
|
+
if (urlProp) return urlProp;
|
|
62
|
+
if (typeof window !== "undefined") return window.location.href;
|
|
63
|
+
return "";
|
|
64
|
+
};
|
|
65
|
+
const handleSubmit = useCallback(
|
|
66
|
+
async (body) => {
|
|
67
|
+
setLoading(true);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch("/api/feedback", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ type, body, url: getUrl(), userId })
|
|
73
|
+
});
|
|
74
|
+
const json = await res.json();
|
|
75
|
+
if (res.ok && json.data) {
|
|
76
|
+
setSubmitted(true);
|
|
77
|
+
onSubmit?.(json.data);
|
|
78
|
+
setTimeout(() => setSubmitted(false), 3e3);
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[type, userId, onSubmit]
|
|
85
|
+
);
|
|
86
|
+
if (submitted) {
|
|
87
|
+
return /* @__PURE__ */ jsx(
|
|
88
|
+
"div",
|
|
89
|
+
{
|
|
90
|
+
"data-theme": theme,
|
|
91
|
+
className,
|
|
92
|
+
style: { ...theme === "dark" ? darkTheme : lightTheme, padding: "12px", borderRadius: "var(--cyguin-radius)", background: "var(--cyguin-bg)", border: "1px solid var(--cyguin-border)", boxShadow: "var(--cyguin-shadow)", fontFamily: "system-ui, sans-serif", color: "var(--cyguin-fg)", textAlign: "center" },
|
|
93
|
+
children: "Thanks for your feedback!"
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (type === "thumbs") {
|
|
98
|
+
return /* @__PURE__ */ jsxs(
|
|
99
|
+
"div",
|
|
100
|
+
{
|
|
101
|
+
"data-theme": theme,
|
|
102
|
+
className,
|
|
103
|
+
style: { ...theme === "dark" ? darkTheme : lightTheme, display: "flex", gap: "8px", padding: "12px", borderRadius: "var(--cyguin-radius)", background: "var(--cyguin-bg)", border: "1px solid var(--cyguin-border)", boxShadow: "var(--cyguin-shadow)", fontFamily: "system-ui, sans-serif" },
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
onClick: () => handleSubmit(THUMBS_UP),
|
|
109
|
+
disabled: loading,
|
|
110
|
+
"aria-label": "Thumbs up",
|
|
111
|
+
style: { background: "none", border: "none", cursor: "pointer", color: "var(--cyguin-fg)", padding: "4px", borderRadius: "var(--cyguin-radius)", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
112
|
+
children: /* @__PURE__ */ jsx(ThumbsUpIcon, {})
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
onClick: () => handleSubmit(THUMBS_DOWN),
|
|
119
|
+
disabled: loading,
|
|
120
|
+
"aria-label": "Thumbs down",
|
|
121
|
+
style: { background: "none", border: "none", cursor: "pointer", color: "var(--cyguin-fg)", padding: "4px", borderRadius: "var(--cyguin-radius)", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
122
|
+
children: /* @__PURE__ */ jsx(ThumbsDownIcon, {})
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (type === "rating") {
|
|
130
|
+
const [selected, setSelected] = useState(null);
|
|
131
|
+
const handleStarClick = (star) => {
|
|
132
|
+
setSelected(star);
|
|
133
|
+
handleSubmit(String(star));
|
|
134
|
+
};
|
|
135
|
+
return /* @__PURE__ */ jsx(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
"data-theme": theme,
|
|
139
|
+
className,
|
|
140
|
+
style: { ...theme === "dark" ? darkTheme : lightTheme, display: "flex", gap: "4px", padding: "12px", borderRadius: "var(--cyguin-radius)", background: "var(--cyguin-bg)", border: "1px solid var(--cyguin-border)", boxShadow: "var(--cyguin-shadow)", fontFamily: "system-ui, sans-serif" },
|
|
141
|
+
children: Array.from({ length: maxStars }, (_, i) => {
|
|
142
|
+
const star = i + 1;
|
|
143
|
+
const filled = selected !== null && star <= selected;
|
|
144
|
+
return /* @__PURE__ */ jsx(
|
|
145
|
+
"button",
|
|
146
|
+
{
|
|
147
|
+
onClick: () => handleStarClick(star),
|
|
148
|
+
disabled: loading,
|
|
149
|
+
"aria-label": `${star} star${star > 1 ? "s" : ""}`,
|
|
150
|
+
style: { background: "none", border: "none", cursor: "pointer", color: filled ? "var(--cyguin-accent)" : "var(--cyguin-fg-muted)", padding: "2px", borderRadius: "var(--cyguin-radius)", display: "flex", alignItems: "center", transition: "color 0.15s" },
|
|
151
|
+
children: /* @__PURE__ */ jsx(StarIcon, { filled })
|
|
152
|
+
},
|
|
153
|
+
star
|
|
154
|
+
);
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (type === "text") {
|
|
160
|
+
const handleTextSubmit = (e) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
if (textValue.trim()) {
|
|
163
|
+
handleSubmit(textValue.trim());
|
|
164
|
+
setTextValue("");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return /* @__PURE__ */ jsx(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
"data-theme": theme,
|
|
171
|
+
className,
|
|
172
|
+
style: { ...theme === "dark" ? darkTheme : lightTheme, padding: "12px", borderRadius: "var(--cyguin-radius)", background: "var(--cyguin-bg)", border: "1px solid var(--cyguin-border)", boxShadow: "var(--cyguin-shadow)", fontFamily: "system-ui, sans-serif" },
|
|
173
|
+
children: /* @__PURE__ */ jsxs("form", { onSubmit: handleTextSubmit, style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
|
|
174
|
+
/* @__PURE__ */ jsx(
|
|
175
|
+
"textarea",
|
|
176
|
+
{
|
|
177
|
+
value: textValue,
|
|
178
|
+
onChange: (e) => setTextValue(e.target.value),
|
|
179
|
+
placeholder,
|
|
180
|
+
rows: 3,
|
|
181
|
+
style: { width: "100%", padding: "8px", borderRadius: "var(--cyguin-radius)", border: "1px solid var(--cyguin-border)", background: "var(--cyguin-bg-subtle)", color: "var(--cyguin-fg)", fontSize: "14px", resize: "vertical", outline: "none", boxSizing: "border-box", fontFamily: "inherit" }
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
/* @__PURE__ */ jsx(
|
|
185
|
+
"button",
|
|
186
|
+
{
|
|
187
|
+
type: "submit",
|
|
188
|
+
disabled: loading || !textValue.trim(),
|
|
189
|
+
style: { alignSelf: "flex-end", padding: "6px 16px", borderRadius: "var(--cyguin-radius)", border: "none", background: "var(--cyguin-accent)", color: "var(--cyguin-accent-fg)", fontWeight: 600, fontSize: "14px", cursor: "pointer", opacity: loading || !textValue.trim() ? 0.5 : 1 },
|
|
190
|
+
children: loading ? "Sending..." : "Submit"
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
] })
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
FeedbackWidget
|
|
202
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/adapters/in-memory.ts
|
|
2
|
+
var _nanoid = require('nanoid');
|
|
3
|
+
var InMemoryFeedbackAdapter = class {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
async list(opts) {
|
|
8
|
+
let records = Array.from(this.store.values());
|
|
9
|
+
if (_optionalChain([opts, 'optionalAccess', _ => _.url]) !== void 0) {
|
|
10
|
+
records = records.filter((r) => r.url === opts.url);
|
|
11
|
+
}
|
|
12
|
+
if (_optionalChain([opts, 'optionalAccess', _2 => _2.reviewed]) !== void 0) {
|
|
13
|
+
records = records.filter((r) => r.reviewed === opts.reviewed);
|
|
14
|
+
}
|
|
15
|
+
records.sort((a, b) => b.createdAt - a.createdAt);
|
|
16
|
+
if (_optionalChain([opts, 'optionalAccess', _3 => _3.limit]) !== void 0) {
|
|
17
|
+
records = records.slice(0, opts.limit);
|
|
18
|
+
}
|
|
19
|
+
return { data: records, total: records.length };
|
|
20
|
+
}
|
|
21
|
+
async create(record) {
|
|
22
|
+
const feedbackRecord = {
|
|
23
|
+
id: _nanoid.nanoid.call(void 0, ),
|
|
24
|
+
userId: record.userId,
|
|
25
|
+
type: record.type,
|
|
26
|
+
body: record.body,
|
|
27
|
+
url: record.url,
|
|
28
|
+
createdAt: record.createdAt,
|
|
29
|
+
reviewed: false
|
|
30
|
+
};
|
|
31
|
+
this.store.set(feedbackRecord.id, feedbackRecord);
|
|
32
|
+
return feedbackRecord;
|
|
33
|
+
}
|
|
34
|
+
async setReviewed(id, reviewed) {
|
|
35
|
+
const record = this.store.get(id);
|
|
36
|
+
if (!record) {
|
|
37
|
+
throw new Error(`Feedback record ${id} not found`);
|
|
38
|
+
}
|
|
39
|
+
record.reviewed = reviewed;
|
|
40
|
+
return record;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
exports.InMemoryFeedbackAdapter = InMemoryFeedbackAdapter;
|