fi-edback 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/client.cjs +1088 -0
- package/dist/client.d.cts +15 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +1062 -0
- package/dist/index.cjs +414 -0
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +377 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
createFeedbackRouteHandler: () => createFeedbackRouteHandler
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
|
+
|
|
37
|
+
// src/lib/validation.ts
|
|
38
|
+
var import_zod = require("zod");
|
|
39
|
+
function getFeedbackSchema() {
|
|
40
|
+
return import_zod.z.object({
|
|
41
|
+
projectSlug: import_zod.z.string().min(1).max(100),
|
|
42
|
+
pageUrl: import_zod.z.string().url(),
|
|
43
|
+
x: import_zod.z.number().finite(),
|
|
44
|
+
y: import_zod.z.number().finite(),
|
|
45
|
+
message: import_zod.z.string().min(1, "Message is required").max(2e3),
|
|
46
|
+
name: import_zod.z.string().max(100).optional(),
|
|
47
|
+
email: import_zod.z.string().max(200).refine((v) => v === "" || import_zod.z.string().email().safeParse(v).success, {
|
|
48
|
+
message: "Must be a valid email address"
|
|
49
|
+
}).optional(),
|
|
50
|
+
sessionId: import_zod.z.string().min(1).max(128),
|
|
51
|
+
userAgent: import_zod.z.string().max(500).optional(),
|
|
52
|
+
// Honeypot field — must be an empty string; bots fill it in
|
|
53
|
+
website: import_zod.z.literal("")
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lib/db/client.ts
|
|
58
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
59
|
+
async function getNeonClient(databaseUrl) {
|
|
60
|
+
const cached = clientCache.get(databaseUrl);
|
|
61
|
+
if (cached) return cached;
|
|
62
|
+
const { neon } = await import("@neondatabase/serverless");
|
|
63
|
+
const client = neon(databaseUrl);
|
|
64
|
+
clientCache.set(databaseUrl, client);
|
|
65
|
+
return client;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/lib/config.ts
|
|
69
|
+
var RATE_LIMIT_MAX = 5;
|
|
70
|
+
var RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
71
|
+
|
|
72
|
+
// src/lib/db/queries.ts
|
|
73
|
+
async function insertFeedback(sql, payload) {
|
|
74
|
+
const rows = await sql`
|
|
75
|
+
INSERT INTO fi_feedback (
|
|
76
|
+
project_slug,
|
|
77
|
+
page_url,
|
|
78
|
+
x,
|
|
79
|
+
y,
|
|
80
|
+
message,
|
|
81
|
+
name,
|
|
82
|
+
email,
|
|
83
|
+
session_id,
|
|
84
|
+
user_agent,
|
|
85
|
+
ip_address
|
|
86
|
+
) VALUES (
|
|
87
|
+
${payload.projectSlug},
|
|
88
|
+
${payload.pageUrl},
|
|
89
|
+
${payload.x},
|
|
90
|
+
${payload.y},
|
|
91
|
+
${payload.message},
|
|
92
|
+
${payload.name ?? null},
|
|
93
|
+
${payload.email ?? null},
|
|
94
|
+
${payload.sessionId},
|
|
95
|
+
${payload.userAgent ?? null},
|
|
96
|
+
${payload.ipAddress ?? null}
|
|
97
|
+
)
|
|
98
|
+
RETURNING
|
|
99
|
+
id,
|
|
100
|
+
project_slug as "projectSlug",
|
|
101
|
+
page_url as "pageUrl",
|
|
102
|
+
x,
|
|
103
|
+
y,
|
|
104
|
+
message,
|
|
105
|
+
name,
|
|
106
|
+
email,
|
|
107
|
+
session_id as "sessionId",
|
|
108
|
+
user_agent as "userAgent",
|
|
109
|
+
ip_address as "ipAddress",
|
|
110
|
+
created_at as "createdAt"
|
|
111
|
+
`;
|
|
112
|
+
const row = rows[0];
|
|
113
|
+
return {
|
|
114
|
+
...row,
|
|
115
|
+
createdAt: new Date(row.createdAt)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function getFeedbackForPage(sql, projectSlug, pageUrl, sessionId) {
|
|
119
|
+
const rows = await sql`
|
|
120
|
+
SELECT
|
|
121
|
+
id,
|
|
122
|
+
project_slug as "projectSlug",
|
|
123
|
+
page_url as "pageUrl",
|
|
124
|
+
x,
|
|
125
|
+
y,
|
|
126
|
+
message,
|
|
127
|
+
name,
|
|
128
|
+
email,
|
|
129
|
+
session_id as "sessionId",
|
|
130
|
+
user_agent as "userAgent",
|
|
131
|
+
ip_address as "ipAddress",
|
|
132
|
+
created_at as "createdAt"
|
|
133
|
+
FROM fi_feedback
|
|
134
|
+
WHERE project_slug = ${projectSlug}
|
|
135
|
+
AND page_url = ${pageUrl}
|
|
136
|
+
ORDER BY created_at DESC
|
|
137
|
+
`;
|
|
138
|
+
const feedback = rows.map((row) => ({
|
|
139
|
+
...row,
|
|
140
|
+
createdAt: new Date(row.createdAt)
|
|
141
|
+
}));
|
|
142
|
+
if (feedback.length > 0) {
|
|
143
|
+
const feedbackIds = feedback.map((f) => f.id);
|
|
144
|
+
const reactionsMap = await getReactionsForFeedback(
|
|
145
|
+
sql,
|
|
146
|
+
feedbackIds,
|
|
147
|
+
sessionId
|
|
148
|
+
);
|
|
149
|
+
for (const item of feedback) {
|
|
150
|
+
item.reactions = reactionsMap.get(item.id) || [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return feedback;
|
|
154
|
+
}
|
|
155
|
+
async function deleteFeedback(sql, id) {
|
|
156
|
+
await sql`
|
|
157
|
+
DELETE FROM fi_feedback
|
|
158
|
+
WHERE id = ${id}
|
|
159
|
+
`;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
async function isRateLimited(sql, sessionId) {
|
|
163
|
+
const windowStart = new Date(
|
|
164
|
+
Date.now() - RATE_LIMIT_WINDOW_SECONDS * 1e3
|
|
165
|
+
).toISOString();
|
|
166
|
+
const rows = await sql`
|
|
167
|
+
SELECT COUNT(*)::int AS count
|
|
168
|
+
FROM fi_feedback
|
|
169
|
+
WHERE session_id = ${sessionId}
|
|
170
|
+
AND created_at > ${windowStart}::timestamptz
|
|
171
|
+
`;
|
|
172
|
+
const count = rows[0].count;
|
|
173
|
+
return count >= RATE_LIMIT_MAX;
|
|
174
|
+
}
|
|
175
|
+
async function getReactionsForFeedback(sql, feedbackIds, currentSessionId) {
|
|
176
|
+
if (feedbackIds.length === 0) {
|
|
177
|
+
return /* @__PURE__ */ new Map();
|
|
178
|
+
}
|
|
179
|
+
const rows = await sql`
|
|
180
|
+
SELECT
|
|
181
|
+
feedback_id as "feedbackId",
|
|
182
|
+
reaction,
|
|
183
|
+
COUNT(*)::int as count,
|
|
184
|
+
BOOL_OR(session_id = ${currentSessionId}) as "hasReacted"
|
|
185
|
+
FROM fi_feedback_reactions
|
|
186
|
+
WHERE feedback_id = ANY(${feedbackIds})
|
|
187
|
+
GROUP BY feedback_id, reaction
|
|
188
|
+
ORDER BY count DESC
|
|
189
|
+
`;
|
|
190
|
+
const map = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
const typed = row;
|
|
193
|
+
if (!map.has(typed.feedbackId)) {
|
|
194
|
+
map.set(typed.feedbackId, []);
|
|
195
|
+
}
|
|
196
|
+
map.get(typed.feedbackId).push({
|
|
197
|
+
reaction: typed.reaction,
|
|
198
|
+
count: typed.count,
|
|
199
|
+
hasReacted: typed.hasReacted
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return map;
|
|
203
|
+
}
|
|
204
|
+
async function toggleReaction(sql, feedbackId, reaction, sessionId) {
|
|
205
|
+
const existing = await sql`
|
|
206
|
+
SELECT id
|
|
207
|
+
FROM fi_feedback_reactions
|
|
208
|
+
WHERE feedback_id = ${feedbackId}
|
|
209
|
+
AND reaction = ${reaction}
|
|
210
|
+
AND session_id = ${sessionId}
|
|
211
|
+
`;
|
|
212
|
+
if (existing.length > 0) {
|
|
213
|
+
await sql`
|
|
214
|
+
DELETE FROM fi_feedback_reactions
|
|
215
|
+
WHERE feedback_id = ${feedbackId}
|
|
216
|
+
AND reaction = ${reaction}
|
|
217
|
+
AND session_id = ${sessionId}
|
|
218
|
+
`;
|
|
219
|
+
return false;
|
|
220
|
+
} else {
|
|
221
|
+
await sql`
|
|
222
|
+
INSERT INTO fi_feedback_reactions (feedback_id, reaction, session_id)
|
|
223
|
+
VALUES (${feedbackId}, ${reaction}, ${sessionId})
|
|
224
|
+
ON CONFLICT (feedback_id, reaction, session_id) DO NOTHING
|
|
225
|
+
`;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function updateFeedbackPosition(sql, feedbackId, x, y) {
|
|
230
|
+
await sql`
|
|
231
|
+
UPDATE fi_feedback
|
|
232
|
+
SET x = ${x}, y = ${y}
|
|
233
|
+
WHERE id = ${feedbackId}
|
|
234
|
+
`;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/server/route-handler.ts
|
|
239
|
+
function getIpAddress(request) {
|
|
240
|
+
const forwarded = request.headers.get("x-forwarded-for");
|
|
241
|
+
if (forwarded) {
|
|
242
|
+
return forwarded.split(",")[0].trim();
|
|
243
|
+
}
|
|
244
|
+
return request.headers.get("x-real-ip") ?? void 0;
|
|
245
|
+
}
|
|
246
|
+
function createFeedbackRouteHandler() {
|
|
247
|
+
const GET = async (request) => {
|
|
248
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
249
|
+
if (!databaseUrl) {
|
|
250
|
+
console.error("[fi-edback] DATABASE_URL is not set");
|
|
251
|
+
return Response.json({ error: "Server misconfigured" }, { status: 500 });
|
|
252
|
+
}
|
|
253
|
+
const url = new URL(request.url);
|
|
254
|
+
const projectSlug = url.searchParams.get("projectSlug");
|
|
255
|
+
const pageUrl = url.searchParams.get("pageUrl");
|
|
256
|
+
const sessionId = url.searchParams.get("sessionId") || "";
|
|
257
|
+
if (!projectSlug || !pageUrl) {
|
|
258
|
+
return Response.json(
|
|
259
|
+
{ error: "Missing projectSlug or pageUrl query parameter" },
|
|
260
|
+
{ status: 400 }
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const sql = await getNeonClient(databaseUrl);
|
|
264
|
+
try {
|
|
265
|
+
const feedback = await getFeedbackForPage(
|
|
266
|
+
sql,
|
|
267
|
+
projectSlug,
|
|
268
|
+
pageUrl,
|
|
269
|
+
sessionId
|
|
270
|
+
);
|
|
271
|
+
return Response.json({ feedback });
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error("[fi-edback] Database error:", error);
|
|
274
|
+
return Response.json(
|
|
275
|
+
{ error: "Failed to fetch feedback" },
|
|
276
|
+
{ status: 500 }
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const POST = async (request) => {
|
|
281
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
282
|
+
if (!databaseUrl) {
|
|
283
|
+
console.error("[fi-edback] DATABASE_URL is not set");
|
|
284
|
+
return Response.json({ error: "Server misconfigured" }, { status: 500 });
|
|
285
|
+
}
|
|
286
|
+
let body;
|
|
287
|
+
try {
|
|
288
|
+
body = await request.json();
|
|
289
|
+
} catch {
|
|
290
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
291
|
+
}
|
|
292
|
+
const parsed = getFeedbackSchema().safeParse(body);
|
|
293
|
+
if (!parsed.success) {
|
|
294
|
+
return Response.json(
|
|
295
|
+
{
|
|
296
|
+
error: "Validation failed",
|
|
297
|
+
issues: parsed.error.flatten().fieldErrors
|
|
298
|
+
},
|
|
299
|
+
{ status: 400 }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (body.website !== "") {
|
|
303
|
+
return Response.json({ ok: true });
|
|
304
|
+
}
|
|
305
|
+
const sql = await getNeonClient(databaseUrl);
|
|
306
|
+
try {
|
|
307
|
+
const limited = await isRateLimited(sql, parsed.data.sessionId);
|
|
308
|
+
if (limited) {
|
|
309
|
+
return Response.json(
|
|
310
|
+
{ error: "Too many submissions \u2014 please wait before trying again." },
|
|
311
|
+
{ status: 429 }
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const feedback = await insertFeedback(sql, {
|
|
315
|
+
projectSlug: parsed.data.projectSlug,
|
|
316
|
+
pageUrl: parsed.data.pageUrl,
|
|
317
|
+
x: parsed.data.x,
|
|
318
|
+
y: parsed.data.y,
|
|
319
|
+
message: parsed.data.message,
|
|
320
|
+
name: parsed.data.name,
|
|
321
|
+
email: parsed.data.email || void 0,
|
|
322
|
+
sessionId: parsed.data.sessionId,
|
|
323
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
324
|
+
ipAddress: getIpAddress(request)
|
|
325
|
+
});
|
|
326
|
+
return Response.json({ feedback });
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("[fi-edback] Database error:", error);
|
|
329
|
+
return Response.json(
|
|
330
|
+
{ error: "Failed to save feedback" },
|
|
331
|
+
{ status: 500 }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const PATCH = async (request) => {
|
|
336
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
337
|
+
if (!databaseUrl) {
|
|
338
|
+
console.error("[fi-edback] DATABASE_URL is not set");
|
|
339
|
+
return Response.json({ error: "Server misconfigured" }, { status: 500 });
|
|
340
|
+
}
|
|
341
|
+
let body;
|
|
342
|
+
try {
|
|
343
|
+
body = await request.json();
|
|
344
|
+
} catch {
|
|
345
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
346
|
+
}
|
|
347
|
+
const bodyObj = body;
|
|
348
|
+
const sql = await getNeonClient(databaseUrl);
|
|
349
|
+
if (typeof bodyObj.x === "number" && typeof bodyObj.y === "number") {
|
|
350
|
+
const { feedbackId: feedbackId2, x, y } = bodyObj;
|
|
351
|
+
if (!feedbackId2) {
|
|
352
|
+
return Response.json({ error: "Missing feedbackId" }, { status: 400 });
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
await updateFeedbackPosition(sql, feedbackId2, x, y);
|
|
356
|
+
return Response.json({ ok: true });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error("[fi-edback] Database error:", error);
|
|
359
|
+
return Response.json(
|
|
360
|
+
{ error: "Failed to update position" },
|
|
361
|
+
{ status: 500 }
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const { feedbackId, reaction, sessionId } = bodyObj;
|
|
366
|
+
if (!feedbackId || !reaction || !sessionId) {
|
|
367
|
+
return Response.json(
|
|
368
|
+
{ error: "Missing feedbackId, reaction, or sessionId" },
|
|
369
|
+
{ status: 400 }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const added = await toggleReaction(sql, feedbackId, reaction, sessionId);
|
|
374
|
+
return Response.json({ added });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error("[fi-edback] Database error:", error);
|
|
377
|
+
return Response.json(
|
|
378
|
+
{ error: "Failed to toggle reaction" },
|
|
379
|
+
{ status: 500 }
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const DELETE = async (request) => {
|
|
384
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
385
|
+
if (!databaseUrl) {
|
|
386
|
+
console.error("[fi-edback] DATABASE_URL is not set");
|
|
387
|
+
return Response.json({ error: "Server misconfigured" }, { status: 500 });
|
|
388
|
+
}
|
|
389
|
+
const url = new URL(request.url);
|
|
390
|
+
const id = url.searchParams.get("id");
|
|
391
|
+
if (!id) {
|
|
392
|
+
return Response.json(
|
|
393
|
+
{ error: "Missing id query parameter" },
|
|
394
|
+
{ status: 400 }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const sql = await getNeonClient(databaseUrl);
|
|
398
|
+
try {
|
|
399
|
+
await deleteFeedback(sql, id);
|
|
400
|
+
return Response.json({ ok: true });
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error("[fi-edback] Database error:", error);
|
|
403
|
+
return Response.json(
|
|
404
|
+
{ error: "Failed to delete feedback" },
|
|
405
|
+
{ status: 500 }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return { GET, POST, PATCH, DELETE };
|
|
410
|
+
}
|
|
411
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
412
|
+
0 && (module.exports = {
|
|
413
|
+
createFeedbackRouteHandler
|
|
414
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type RouteHandler = (request: Request) => Promise<Response>;
|
|
2
|
+
/**
|
|
3
|
+
* Returns a `{ GET, POST, DELETE }` object ready to be re-exported from a
|
|
4
|
+
* Next.js Route Handler file. The host app creates the file and delegates to
|
|
5
|
+
* this factory — keeping Server Action / Route Handler wiring inside the
|
|
6
|
+
* consuming app while all logic stays in the package.
|
|
7
|
+
*
|
|
8
|
+
* Usage in app/api/fi-edback/route.ts:
|
|
9
|
+
* import { createFeedbackRouteHandler } from 'fi-edback'
|
|
10
|
+
* export const { GET, POST, DELETE } = createFeedbackRouteHandler()
|
|
11
|
+
*/
|
|
12
|
+
declare function createFeedbackRouteHandler(): {
|
|
13
|
+
GET: RouteHandler;
|
|
14
|
+
POST: RouteHandler;
|
|
15
|
+
PATCH: RouteHandler;
|
|
16
|
+
DELETE: RouteHandler;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface FeedbackPayload {
|
|
20
|
+
projectSlug: string;
|
|
21
|
+
pageUrl: string;
|
|
22
|
+
/** Document-relative X coordinate (clientX + scrollX) */
|
|
23
|
+
x: number;
|
|
24
|
+
/** Document-relative Y coordinate (clientY + scrollY) */
|
|
25
|
+
y: number;
|
|
26
|
+
message: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
userAgent?: string;
|
|
31
|
+
ipAddress?: string;
|
|
32
|
+
}
|
|
33
|
+
interface FeedbackConfig {
|
|
34
|
+
apiPath?: string;
|
|
35
|
+
}
|
|
36
|
+
interface FeedbackRow extends FeedbackPayload {
|
|
37
|
+
id: string;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
reactions?: ReactionSummary[];
|
|
40
|
+
}
|
|
41
|
+
interface ReactionSummary {
|
|
42
|
+
reaction: string;
|
|
43
|
+
count: number;
|
|
44
|
+
hasReacted: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { type FeedbackConfig, type FeedbackPayload, type FeedbackRow, createFeedbackRouteHandler };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type RouteHandler = (request: Request) => Promise<Response>;
|
|
2
|
+
/**
|
|
3
|
+
* Returns a `{ GET, POST, DELETE }` object ready to be re-exported from a
|
|
4
|
+
* Next.js Route Handler file. The host app creates the file and delegates to
|
|
5
|
+
* this factory — keeping Server Action / Route Handler wiring inside the
|
|
6
|
+
* consuming app while all logic stays in the package.
|
|
7
|
+
*
|
|
8
|
+
* Usage in app/api/fi-edback/route.ts:
|
|
9
|
+
* import { createFeedbackRouteHandler } from 'fi-edback'
|
|
10
|
+
* export const { GET, POST, DELETE } = createFeedbackRouteHandler()
|
|
11
|
+
*/
|
|
12
|
+
declare function createFeedbackRouteHandler(): {
|
|
13
|
+
GET: RouteHandler;
|
|
14
|
+
POST: RouteHandler;
|
|
15
|
+
PATCH: RouteHandler;
|
|
16
|
+
DELETE: RouteHandler;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface FeedbackPayload {
|
|
20
|
+
projectSlug: string;
|
|
21
|
+
pageUrl: string;
|
|
22
|
+
/** Document-relative X coordinate (clientX + scrollX) */
|
|
23
|
+
x: number;
|
|
24
|
+
/** Document-relative Y coordinate (clientY + scrollY) */
|
|
25
|
+
y: number;
|
|
26
|
+
message: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
userAgent?: string;
|
|
31
|
+
ipAddress?: string;
|
|
32
|
+
}
|
|
33
|
+
interface FeedbackConfig {
|
|
34
|
+
apiPath?: string;
|
|
35
|
+
}
|
|
36
|
+
interface FeedbackRow extends FeedbackPayload {
|
|
37
|
+
id: string;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
reactions?: ReactionSummary[];
|
|
40
|
+
}
|
|
41
|
+
interface ReactionSummary {
|
|
42
|
+
reaction: string;
|
|
43
|
+
count: number;
|
|
44
|
+
hasReacted: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { type FeedbackConfig, type FeedbackPayload, type FeedbackRow, createFeedbackRouteHandler };
|