@fewchurs/react 0.0.0-stage → 1.0.1
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/LICENSE +41 -0
- package/README.md +25 -2
- package/dist/core.d.mts +213 -0
- package/dist/core.d.ts +213 -0
- package/dist/core.js +425 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +375 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +523 -0
- package/dist/index.d.ts +523 -0
- package/dist/index.js +1823 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1742 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +484 -0
- package/package.json +113 -3
package/dist/core.mjs
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// src/client/version.ts
|
|
2
|
+
var SDK_VERSION = "1.0.1";
|
|
3
|
+
|
|
4
|
+
// src/client/constants.ts
|
|
5
|
+
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
6
|
+
var DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
7
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
8
|
+
var LIMITS = {
|
|
9
|
+
titleMin: 3,
|
|
10
|
+
titleMax: 100,
|
|
11
|
+
descriptionMax: 1e3,
|
|
12
|
+
commentMin: 1,
|
|
13
|
+
commentMax: 1e3,
|
|
14
|
+
authorNameMax: 60
|
|
15
|
+
};
|
|
16
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
17
|
+
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
18
|
+
|
|
19
|
+
// src/client/errors.ts
|
|
20
|
+
var USER_MESSAGES = {
|
|
21
|
+
invalid_key: "Feedback is unavailable right now.",
|
|
22
|
+
validation_failed: "Please check what you entered and try again.",
|
|
23
|
+
rate_limited: "You have reached today's limit. Try again tomorrow.",
|
|
24
|
+
not_found: "That request no longer exists.",
|
|
25
|
+
limit_reached: "Feedback is unavailable right now.",
|
|
26
|
+
server: "Something went wrong. Please try again.",
|
|
27
|
+
network: "Could not reach the server. Check your connection."
|
|
28
|
+
};
|
|
29
|
+
function userMessage(error) {
|
|
30
|
+
if (error.code === "validation_failed" && error.message) return error.message;
|
|
31
|
+
return USER_MESSAGES[error.code];
|
|
32
|
+
}
|
|
33
|
+
function networkError(message = "Network request failed.") {
|
|
34
|
+
return { code: "network", message };
|
|
35
|
+
}
|
|
36
|
+
function validationError(message) {
|
|
37
|
+
return { code: "validation_failed", status: 400, message };
|
|
38
|
+
}
|
|
39
|
+
function errorFromResponse(status, body) {
|
|
40
|
+
const code = body?.error?.code;
|
|
41
|
+
const message = body?.error?.message ?? `Request failed (${status}).`;
|
|
42
|
+
switch (code) {
|
|
43
|
+
case "invalid_key":
|
|
44
|
+
return { code: "invalid_key", status: 401, message };
|
|
45
|
+
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
46
|
+
// so it collapses into validation_failed rather than widening the public union.
|
|
47
|
+
case "missing_device_id":
|
|
48
|
+
case "validation_failed":
|
|
49
|
+
return { code: "validation_failed", status: 400, message };
|
|
50
|
+
case "rate_limited":
|
|
51
|
+
return { code: "rate_limited", status: 429, message };
|
|
52
|
+
case "not_found":
|
|
53
|
+
return { code: "not_found", status: 404, message };
|
|
54
|
+
case "limit_reached":
|
|
55
|
+
return { code: "limit_reached", status: 402, message };
|
|
56
|
+
default:
|
|
57
|
+
return status === 401 ? { code: "invalid_key", status: 401, message } : { code: "server", status, message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/client/normalize.ts
|
|
62
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set(["open", "planned", "in_progress", "done", "declined"]);
|
|
63
|
+
function asRecord(value) {
|
|
64
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
65
|
+
}
|
|
66
|
+
function str(value, fallback = "") {
|
|
67
|
+
return typeof value === "string" ? value : fallback;
|
|
68
|
+
}
|
|
69
|
+
function num(value) {
|
|
70
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
71
|
+
}
|
|
72
|
+
function bool(value) {
|
|
73
|
+
return value === true;
|
|
74
|
+
}
|
|
75
|
+
function normalizeStatus(value) {
|
|
76
|
+
return typeof value === "string" && KNOWN_STATUSES.has(value) ? value : "unknown";
|
|
77
|
+
}
|
|
78
|
+
function isMine(raw, ownerKey, deviceId) {
|
|
79
|
+
if (typeof raw.isMine === "boolean") return raw.isMine;
|
|
80
|
+
const owner = raw[ownerKey];
|
|
81
|
+
return typeof owner === "string" && owner.length > 0 && owner === deviceId;
|
|
82
|
+
}
|
|
83
|
+
function normalizeFeature(value, deviceId) {
|
|
84
|
+
const raw = asRecord(value);
|
|
85
|
+
return {
|
|
86
|
+
id: str(raw.id),
|
|
87
|
+
title: str(raw.title),
|
|
88
|
+
description: str(raw.description),
|
|
89
|
+
status: normalizeStatus(raw.status),
|
|
90
|
+
upvoteCount: num(raw.upvoteCount),
|
|
91
|
+
commentCount: num(raw.commentCount),
|
|
92
|
+
followerCount: num(raw.followerCount),
|
|
93
|
+
hasVoted: bool(raw.hasVoted),
|
|
94
|
+
isFollowing: bool(raw.isFollowing),
|
|
95
|
+
isMine: isMine(raw, "authorDeviceId", deviceId),
|
|
96
|
+
createdAt: str(raw.createdAt),
|
|
97
|
+
updatedAt: str(raw.updatedAt, str(raw.createdAt))
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function normalizeComment(value, deviceId) {
|
|
101
|
+
const raw = asRecord(value);
|
|
102
|
+
return {
|
|
103
|
+
id: str(raw.id),
|
|
104
|
+
text: str(raw.text),
|
|
105
|
+
authorName: str(raw.authorName, "App User"),
|
|
106
|
+
isDeveloper: bool(raw.isDeveloper),
|
|
107
|
+
isMine: isMine(raw, "deviceId", deviceId),
|
|
108
|
+
createdAt: str(raw.createdAt)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function normalizeFeaturePage(value, deviceId) {
|
|
112
|
+
const raw = asRecord(value);
|
|
113
|
+
const features = Array.isArray(raw.features) ? raw.features : [];
|
|
114
|
+
return {
|
|
115
|
+
features: features.map((feature) => normalizeFeature(feature, deviceId)),
|
|
116
|
+
nextCursor: typeof raw.nextCursor === "string" ? raw.nextCursor : null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeCommentPage(value, deviceId) {
|
|
120
|
+
const raw = asRecord(value);
|
|
121
|
+
const comments = Array.isArray(raw.comments) ? raw.comments : [];
|
|
122
|
+
return {
|
|
123
|
+
comments: comments.map((comment) => normalizeComment(comment, deviceId)),
|
|
124
|
+
nextCursor: typeof raw.nextCursor === "string" ? raw.nextCursor : null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function normalizeConfig(value) {
|
|
128
|
+
const raw = asRecord(value);
|
|
129
|
+
return {
|
|
130
|
+
showBranding: raw.showBranding !== false,
|
|
131
|
+
hideVoteCounts: bool(raw.hideVoteCounts),
|
|
132
|
+
appName: typeof raw.appName === "string" && raw.appName ? raw.appName : null
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function normalizeVote(value) {
|
|
136
|
+
const raw = asRecord(value);
|
|
137
|
+
return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/client/validate.ts
|
|
141
|
+
function validateFeatureInput(input) {
|
|
142
|
+
const title = input.title.trim();
|
|
143
|
+
if (title.length < LIMITS.titleMin) {
|
|
144
|
+
return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);
|
|
145
|
+
}
|
|
146
|
+
if (title.length > LIMITS.titleMax) {
|
|
147
|
+
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
148
|
+
}
|
|
149
|
+
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
150
|
+
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
151
|
+
}
|
|
152
|
+
if (input.email !== void 0 && input.email !== "" && !isEmail(input.email)) {
|
|
153
|
+
return validationError("Enter a valid email address.");
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function validateCommentInput(input) {
|
|
158
|
+
const text = input.text.trim();
|
|
159
|
+
if (text.length < LIMITS.commentMin) return validationError("Write a comment first.");
|
|
160
|
+
if (text.length > LIMITS.commentMax) {
|
|
161
|
+
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
162
|
+
}
|
|
163
|
+
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
164
|
+
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function validateEmail(email) {
|
|
169
|
+
return isEmail(email) ? null : validationError("Enter a valid email address.");
|
|
170
|
+
}
|
|
171
|
+
function isEmail(email) {
|
|
172
|
+
return EMAIL_PATTERN.test(email.trim());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/client/client.ts
|
|
176
|
+
function createFewchursClient(options = {}) {
|
|
177
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
178
|
+
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
179
|
+
const retries = options.retries ?? 1;
|
|
180
|
+
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
181
|
+
function getDeviceId() {
|
|
182
|
+
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
183
|
+
}
|
|
184
|
+
function headers(hasBody) {
|
|
185
|
+
const result = {
|
|
186
|
+
Accept: "application/json",
|
|
187
|
+
"X-Fewchurs-Sdk": SDK_HEADER
|
|
188
|
+
};
|
|
189
|
+
if (hasBody) result["Content-Type"] = "application/json";
|
|
190
|
+
if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;
|
|
191
|
+
const deviceId = getDeviceId();
|
|
192
|
+
if (deviceId) result["X-Device-Id"] = deviceId;
|
|
193
|
+
if (options.language) result["Accept-Language"] = options.language;
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
function url(path, query) {
|
|
197
|
+
const search = new URLSearchParams();
|
|
198
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
199
|
+
if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
|
|
200
|
+
}
|
|
201
|
+
const qs = search.toString();
|
|
202
|
+
return `${baseUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
203
|
+
}
|
|
204
|
+
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
205
|
+
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
206
|
+
const target = url(path, query);
|
|
207
|
+
const init = { method, headers: headers(body !== void 0) };
|
|
208
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
209
|
+
if (signal) init.signal = signal;
|
|
210
|
+
const attempts = method === "GET" ? retries + 1 : 1;
|
|
211
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
212
|
+
let response;
|
|
213
|
+
try {
|
|
214
|
+
response = await doFetch(target, init);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (signal?.aborted) return { ok: false, error: networkError("Request aborted.") };
|
|
217
|
+
if (attempt < attempts - 1) {
|
|
218
|
+
await delay(retryDelayMs);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
return { ok: false, error: networkError(messageOf(error)) };
|
|
222
|
+
}
|
|
223
|
+
if (response.ok) {
|
|
224
|
+
const payload = await readJson(response);
|
|
225
|
+
return { ok: true, data: map(payload) };
|
|
226
|
+
}
|
|
227
|
+
if (response.status >= 500 && attempt < attempts - 1) {
|
|
228
|
+
await delay(retryDelayMs);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const failure = errorFromResponse(response.status, await readJson(response));
|
|
232
|
+
warnAboutKey(failure.code);
|
|
233
|
+
return { ok: false, error: failure };
|
|
234
|
+
}
|
|
235
|
+
return { ok: false, error: networkError() };
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
baseUrl,
|
|
239
|
+
getDeviceId,
|
|
240
|
+
getConfig: (signal) => request("/config", normalizeConfig, { signal }),
|
|
241
|
+
listFeatures: (params = {}, signal) => request("/features", (body) => normalizeFeaturePage(body, getDeviceId()), {
|
|
242
|
+
query: {
|
|
243
|
+
sort: params.sort ?? "new",
|
|
244
|
+
limit: params.limit ?? DEFAULT_PAGE_SIZE,
|
|
245
|
+
cursor: params.cursor ?? void 0,
|
|
246
|
+
status: params.status?.length ? params.status.join(",") : void 0
|
|
247
|
+
},
|
|
248
|
+
signal
|
|
249
|
+
}),
|
|
250
|
+
getFeature: (featureId, signal) => request(
|
|
251
|
+
`/features/${encodeURIComponent(featureId)}`,
|
|
252
|
+
(body) => normalizeFeature(body, getDeviceId()),
|
|
253
|
+
{ signal }
|
|
254
|
+
),
|
|
255
|
+
submitFeature: async (input) => {
|
|
256
|
+
const invalid = validateFeatureInput(input);
|
|
257
|
+
if (invalid) return { ok: false, error: invalid };
|
|
258
|
+
return request("/features", (body) => normalizeFeature(body, getDeviceId()), {
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: {
|
|
261
|
+
title: input.title.trim(),
|
|
262
|
+
description: (input.description ?? "").trim(),
|
|
263
|
+
...input.email ? { email: input.email.trim() } : {},
|
|
264
|
+
isSubscriber: input.isSubscriber ?? false
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
vote: (featureId) => request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: "POST" }),
|
|
269
|
+
unvote: (featureId) => request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {
|
|
270
|
+
method: "DELETE"
|
|
271
|
+
}),
|
|
272
|
+
follow: async (featureId, email) => {
|
|
273
|
+
const invalid = validateEmail(email);
|
|
274
|
+
if (invalid) return { ok: false, error: invalid };
|
|
275
|
+
return request(
|
|
276
|
+
`/features/${encodeURIComponent(featureId)}/follow`,
|
|
277
|
+
() => ({ following: true }),
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: { email: email.trim() }
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
unfollow: (featureId) => request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {
|
|
285
|
+
method: "DELETE"
|
|
286
|
+
}),
|
|
287
|
+
listComments: (featureId, params = {}, signal) => request(
|
|
288
|
+
`/features/${encodeURIComponent(featureId)}/comments`,
|
|
289
|
+
(body) => normalizeCommentPage(body, getDeviceId()),
|
|
290
|
+
{
|
|
291
|
+
query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? void 0 },
|
|
292
|
+
signal
|
|
293
|
+
}
|
|
294
|
+
),
|
|
295
|
+
// The server takes the feature id in the body here; POST /features/{id}/comments is a 405.
|
|
296
|
+
createComment: async (input) => {
|
|
297
|
+
const invalid = validateCommentInput(input);
|
|
298
|
+
if (invalid) return { ok: false, error: invalid };
|
|
299
|
+
return request("/comments", (body) => normalizeComment(body, getDeviceId()), {
|
|
300
|
+
method: "POST",
|
|
301
|
+
body: {
|
|
302
|
+
featureId: input.featureId,
|
|
303
|
+
text: input.text.trim(),
|
|
304
|
+
...input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function readJson(response) {
|
|
311
|
+
try {
|
|
312
|
+
return await response.json();
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function delay(ms) {
|
|
318
|
+
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
319
|
+
}
|
|
320
|
+
function messageOf(error) {
|
|
321
|
+
return error instanceof Error ? error.message : "Network request failed.";
|
|
322
|
+
}
|
|
323
|
+
var warnedAboutKey = false;
|
|
324
|
+
function warnAboutKey(code) {
|
|
325
|
+
if (code !== "invalid_key" || warnedAboutKey) return;
|
|
326
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
327
|
+
warnedAboutKey = true;
|
|
328
|
+
console.error(
|
|
329
|
+
"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/identity/deviceId.ts
|
|
334
|
+
var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
335
|
+
function createDeviceId() {
|
|
336
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
337
|
+
return crypto.randomUUID();
|
|
338
|
+
}
|
|
339
|
+
return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/store/keys.ts
|
|
343
|
+
function listKey({ sort, status }) {
|
|
344
|
+
return status?.length ? `${sort}:${[...status].sort().join(",")}` : sort;
|
|
345
|
+
}
|
|
346
|
+
function featureKey(featureId) {
|
|
347
|
+
return `feature:${featureId}`;
|
|
348
|
+
}
|
|
349
|
+
export {
|
|
350
|
+
DEFAULT_BASE_URL,
|
|
351
|
+
DEFAULT_PAGE_SIZE,
|
|
352
|
+
LIMITS,
|
|
353
|
+
ROADMAP_STATUSES,
|
|
354
|
+
SDK_HEADER,
|
|
355
|
+
SDK_VERSION,
|
|
356
|
+
createDeviceId,
|
|
357
|
+
createFewchursClient,
|
|
358
|
+
errorFromResponse,
|
|
359
|
+
featureKey,
|
|
360
|
+
listKey,
|
|
361
|
+
networkError,
|
|
362
|
+
normalizeComment,
|
|
363
|
+
normalizeCommentPage,
|
|
364
|
+
normalizeConfig,
|
|
365
|
+
normalizeFeature,
|
|
366
|
+
normalizeFeaturePage,
|
|
367
|
+
normalizeStatus,
|
|
368
|
+
normalizeVote,
|
|
369
|
+
userMessage,
|
|
370
|
+
validateCommentInput,
|
|
371
|
+
validateEmail,
|
|
372
|
+
validateFeatureInput,
|
|
373
|
+
validationError
|
|
374
|
+
};
|
|
375
|
+
//# sourceMappingURL=core.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/version.ts","../src/client/constants.ts","../src/client/errors.ts","../src/client/normalize.ts","../src/client/validate.ts","../src/client/client.ts","../src/identity/deviceId.ts","../src/store/keys.ts"],"sourcesContent":["// Generated by tools/stamp-version.mjs from @fewchurs/react's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import type { FeatureStatus } from \"../types\";\nimport { SDK_VERSION } from \"./version\";\n\nexport { SDK_VERSION };\nexport const SDK_HEADER = `react/${SDK_VERSION}`;\nexport const DEFAULT_BASE_URL = \"https://www.fewchurs.com/api/v1\";\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */\nexport const LIMITS = {\n titleMin: 3,\n titleMax: 100,\n descriptionMax: 1000,\n commentMin: 1,\n commentMax: 1000,\n authorNameMax: 60,\n} as const;\n\n/** Deliberately loose: the server owns the real check, this only catches obvious typos. */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */\nexport const ROADMAP_STATUSES: FeatureStatus[] = [\"planned\", \"in_progress\", \"done\"];\n","export type FewchursError =\n | { code: \"invalid_key\"; status: 401; message: string }\n | { code: \"validation_failed\"; status: 400; message: string }\n | { code: \"rate_limited\"; status: 429; message: string }\n | { code: \"not_found\"; status: 404; message: string }\n | { code: \"limit_reached\"; status: 402; message: string }\n | { code: \"server\"; status: number; message: string }\n | { code: \"network\"; message: string };\n\nexport type Result<T> = { ok: true; data: T } | { ok: false; error: FewchursError };\n\ninterface ServerErrorBody {\n error?: { code?: string; message?: string };\n}\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Feedback is unavailable right now.\",\n validation_failed: \"Please check what you entered and try again.\",\n rate_limited: \"You have reached today's limit. Try again tomorrow.\",\n not_found: \"That request no longer exists.\",\n limit_reached: \"Feedback is unavailable right now.\",\n server: \"Something went wrong. Please try again.\",\n network: \"Could not reach the server. Check your connection.\",\n};\n\n/** Safe to show a visitor: never leaks a server message that may name internals. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n return USER_MESSAGES[error.code];\n}\n\nexport function networkError(message = \"Network request failed.\"): FewchursError {\n return { code: \"network\", message };\n}\n\nexport function validationError(message: string): FewchursError {\n return { code: \"validation_failed\", status: 400, message };\n}\n\nexport function errorFromResponse(status: number, body: unknown): FewchursError {\n const code = (body as ServerErrorBody | null)?.error?.code;\n const message = (body as ServerErrorBody | null)?.error?.message ?? `Request failed (${status}).`;\n\n switch (code) {\n case \"invalid_key\":\n return { code: \"invalid_key\", status: 401, message };\n // missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,\n // so it collapses into validation_failed rather than widening the public union.\n case \"missing_device_id\":\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message };\n case \"rate_limited\":\n return { code: \"rate_limited\", status: 429, message };\n case \"not_found\":\n return { code: \"not_found\", status: 404, message };\n case \"limit_reached\":\n return { code: \"limit_reached\", status: 402, message };\n default:\n return status === 401\n ? { code: \"invalid_key\", status: 401, message }\n : { code: \"server\", status, message };\n }\n}\n","import type {\n Comment,\n CommentPage,\n Feature,\n FeaturePage,\n FeatureStatus,\n FewchursConfig,\n} from \"../types\";\n\nconst KNOWN_STATUSES = new Set<string>([\"open\", \"planned\", \"in_progress\", \"done\", \"declined\"]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : {};\n}\n\nfunction str(value: unknown, fallback = \"\"): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\nfunction bool(value: unknown): boolean {\n return value === true;\n}\n\nexport function normalizeStatus(value: unknown): FeatureStatus {\n return typeof value === \"string\" && KNOWN_STATUSES.has(value)\n ? (value as FeatureStatus)\n : \"unknown\";\n}\n\n// `isMine` is the server's answer once it ships; until then the raw author device id is the\n// only signal, so it is compared here and never surfaced on the public type.\nfunction isMine(raw: Record<string, unknown>, ownerKey: string, deviceId: string | null): boolean {\n if (typeof raw.isMine === \"boolean\") return raw.isMine;\n const owner = raw[ownerKey];\n return typeof owner === \"string\" && owner.length > 0 && owner === deviceId;\n}\n\nexport function normalizeFeature(value: unknown, deviceId: string | null): Feature {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n title: str(raw.title),\n description: str(raw.description),\n status: normalizeStatus(raw.status),\n upvoteCount: num(raw.upvoteCount),\n commentCount: num(raw.commentCount),\n followerCount: num(raw.followerCount),\n hasVoted: bool(raw.hasVoted),\n isFollowing: bool(raw.isFollowing),\n isMine: isMine(raw, \"authorDeviceId\", deviceId),\n createdAt: str(raw.createdAt),\n updatedAt: str(raw.updatedAt, str(raw.createdAt)),\n };\n}\n\nexport function normalizeComment(value: unknown, deviceId: string | null): Comment {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n text: str(raw.text),\n authorName: str(raw.authorName, \"App User\"),\n isDeveloper: bool(raw.isDeveloper),\n isMine: isMine(raw, \"deviceId\", deviceId),\n createdAt: str(raw.createdAt),\n };\n}\n\nexport function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage {\n const raw = asRecord(value);\n const features = Array.isArray(raw.features) ? raw.features : [];\n return {\n features: features.map((feature) => normalizeFeature(feature, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage {\n const raw = asRecord(value);\n const comments = Array.isArray(raw.comments) ? raw.comments : [];\n return {\n comments: comments.map((comment) => normalizeComment(comment, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeConfig(value: unknown): FewchursConfig {\n const raw = asRecord(value);\n return {\n showBranding: raw.showBranding !== false,\n hideVoteCounts: bool(raw.hideVoteCounts),\n appName: typeof raw.appName === \"string\" && raw.appName ? raw.appName : null,\n };\n}\n\nexport function normalizeVote(value: unknown): { upvoteCount: number; hasVoted: boolean } {\n const raw = asRecord(value);\n return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };\n}\n","import { EMAIL_PATTERN, LIMITS } from \"./constants\";\nimport { validationError, type FewchursError } from \"./errors\";\nimport type { CreateCommentInput, SubmitFeatureInput } from \"../types\";\n\nexport function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null {\n const title = input.title.trim();\n if (title.length < LIMITS.titleMin) {\n return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);\n }\n if (title.length > LIMITS.titleMax) {\n return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);\n }\n if ((input.description ?? \"\").trim().length > LIMITS.descriptionMax) {\n return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);\n }\n if (input.email !== undefined && input.email !== \"\" && !isEmail(input.email)) {\n return validationError(\"Enter a valid email address.\");\n }\n return null;\n}\n\nexport function validateCommentInput(input: CreateCommentInput): FewchursError | null {\n const text = input.text.trim();\n if (text.length < LIMITS.commentMin) return validationError(\"Write a comment first.\");\n if (text.length > LIMITS.commentMax) {\n return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);\n }\n if ((input.authorName ?? \"\").trim().length > LIMITS.authorNameMax) {\n return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);\n }\n return null;\n}\n\nexport function validateEmail(email: string): FewchursError | null {\n return isEmail(email) ? null : validationError(\"Enter a valid email address.\");\n}\n\nfunction isEmail(email: string): boolean {\n return EMAIL_PATTERN.test(email.trim());\n}\n","import { DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, SDK_HEADER } from \"./constants\";\nimport { errorFromResponse, networkError, type Result } from \"./errors\";\nimport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeVote,\n} from \"./normalize\";\nimport { validateCommentInput, validateEmail, validateFeatureInput } from \"./validate\";\nimport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"../types\";\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface FewchursClientOptions {\n /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */\n apiKey?: string;\n /** `https://www.fewchurs.com/api/v1` by default, or a proxy path such as `/api/fewchurs`. */\n baseUrl?: string;\n deviceId?: string | null | (() => string | null);\n /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */\n language?: string;\n fetch?: FetchLike;\n retries?: number;\n retryDelayMs?: number;\n}\n\nexport interface FewchursClient {\n readonly baseUrl: string;\n getDeviceId(): string | null;\n getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;\n listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;\n getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;\n submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;\n vote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n unvote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n follow(featureId: string, email: string): Promise<Result<{ following: boolean }>>;\n unfollow(featureId: string): Promise<Result<{ following: boolean }>>;\n listComments(\n featureId: string,\n params?: ListCommentsParams,\n signal?: AbortSignal,\n ): Promise<Result<CommentPage>>;\n createComment(input: CreateCommentInput): Promise<Result<Comment>>;\n}\n\ninterface RequestOptions {\n method?: \"GET\" | \"POST\" | \"DELETE\";\n body?: unknown;\n query?: Record<string, string | number | undefined | null>;\n signal?: AbortSignal;\n}\n\nexport function createFewchursClient(options: FewchursClientOptions = {}): FewchursClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);\n const retries = options.retries ?? 1;\n const retryDelayMs = options.retryDelayMs ?? 250;\n\n function getDeviceId(): string | null {\n return typeof options.deviceId === \"function\" ? options.deviceId() : (options.deviceId ?? null);\n }\n\n function headers(hasBody: boolean): Record<string, string> {\n const result: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Fewchurs-Sdk\": SDK_HEADER,\n };\n if (hasBody) result[\"Content-Type\"] = \"application/json\";\n if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;\n const deviceId = getDeviceId();\n if (deviceId) result[\"X-Device-Id\"] = deviceId;\n if (options.language) result[\"Accept-Language\"] = options.language;\n return result;\n }\n\n function url(path: string, query?: RequestOptions[\"query\"]): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== null && value !== \"\") search.set(key, String(value));\n }\n const qs = search.toString();\n return `${baseUrl}${path}${qs ? `?${qs}` : \"\"}`;\n }\n\n async function request<T>(\n path: string,\n map: (body: unknown) => T,\n { method = \"GET\", body, query, signal }: RequestOptions = {},\n ): Promise<Result<T>> {\n if (!doFetch) return { ok: false, error: networkError(\"No fetch implementation available.\") };\n\n const target = url(path, query);\n const init: RequestInit = { method, headers: headers(body !== undefined) };\n if (body !== undefined) init.body = JSON.stringify(body);\n if (signal) init.signal = signal;\n\n // Only GETs are retried: a retried POST could double-submit a request or a comment.\n const attempts = method === \"GET\" ? retries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n let response: Response;\n try {\n response = await doFetch(target, init);\n } catch (error) {\n if (signal?.aborted) return { ok: false, error: networkError(\"Request aborted.\") };\n if (attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n return { ok: false, error: networkError(messageOf(error)) };\n }\n\n if (response.ok) {\n const payload = await readJson(response);\n return { ok: true, data: map(payload) };\n }\n\n if (response.status >= 500 && attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n\n const failure = errorFromResponse(response.status, await readJson(response));\n warnAboutKey(failure.code);\n return { ok: false, error: failure };\n }\n\n return { ok: false, error: networkError() };\n }\n\n return {\n baseUrl,\n getDeviceId,\n\n getConfig: (signal) => request(\"/config\", normalizeConfig, { signal }),\n\n listFeatures: (params = {}, signal) =>\n request(\"/features\", (body) => normalizeFeaturePage(body, getDeviceId()), {\n query: {\n sort: params.sort ?? \"new\",\n limit: params.limit ?? DEFAULT_PAGE_SIZE,\n cursor: params.cursor ?? undefined,\n status: params.status?.length ? params.status.join(\",\") : undefined,\n },\n signal,\n }),\n\n getFeature: (featureId, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}`,\n (body) => normalizeFeature(body, getDeviceId()),\n { signal },\n ),\n\n submitFeature: async (input) => {\n const invalid = validateFeatureInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/features\", (body) => normalizeFeature(body, getDeviceId()), {\n method: \"POST\",\n body: {\n title: input.title.trim(),\n description: (input.description ?? \"\").trim(),\n ...(input.email ? { email: input.email.trim() } : {}),\n isSubscriber: input.isSubscriber ?? false,\n },\n });\n },\n\n vote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: \"POST\" }),\n\n unvote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {\n method: \"DELETE\",\n }),\n\n follow: async (featureId, email) => {\n const invalid = validateEmail(email);\n if (invalid) return { ok: false, error: invalid };\n return request(\n `/features/${encodeURIComponent(featureId)}/follow`,\n () => ({ following: true }),\n {\n method: \"POST\",\n body: { email: email.trim() },\n },\n );\n },\n\n unfollow: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {\n method: \"DELETE\",\n }),\n\n listComments: (featureId, params = {}, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}/comments`,\n (body) => normalizeCommentPage(body, getDeviceId()),\n {\n query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? undefined },\n signal,\n },\n ),\n\n // The server takes the feature id in the body here; POST /features/{id}/comments is a 405.\n createComment: async (input) => {\n const invalid = validateCommentInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/comments\", (body) => normalizeComment(body, getDeviceId()), {\n method: \"POST\",\n body: {\n featureId: input.featureId,\n text: input.text.trim(),\n ...(input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}),\n },\n });\n },\n };\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : \"Network request failed.\";\n}\n\n// Declared, not imported from @types/node: this is a browser package, and bundlers replace\n// `process.env.NODE_ENV` inline, so the check costs nothing in a production build.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet warnedAboutKey = false;\n\nfunction warnAboutKey(code: string): void {\n if (code !== \"invalid_key\" || warnedAboutKey) return;\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n warnedAboutKey = true;\n console.error(\n \"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard.\",\n );\n}\n","const COOKIE_NAME = \"fewchurs_did\";\nconst STORAGE_KEY = \"fewchurs_device_id\";\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nexport function createDeviceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport function readDeviceId(): string | null {\n if (typeof document === \"undefined\") return null;\n return readCookie() ?? readStorage();\n}\n\n/**\n * Cookie first, storage second: a cookie is the only copy a same-origin server can read, so\n * the board renders with the same identity on both sides and never mismatches on hydration.\n */\nexport function resolveDeviceId(): string {\n const existing = readDeviceId();\n if (existing) {\n persist(existing);\n return existing;\n }\n const created = createDeviceId();\n persist(created);\n return created;\n}\n\nfunction persist(deviceId: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${COOKIE_NAME}=${encodeURIComponent(deviceId)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;\n } catch {\n // Cookies can be blocked entirely; storage below is the fallback.\n }\n try {\n localStorage.setItem(STORAGE_KEY, deviceId);\n } catch {\n // Private-mode storage throws on write; the cookie or a fresh id per visit still works.\n }\n}\n\nfunction readCookie(): string | null {\n const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction readStorage(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nexport { COOKIE_NAME as DEVICE_ID_COOKIE };\n","import type { FeatureStatus, FeatureSort } from \"../types\";\n\nexport interface ListQuery {\n sort: FeatureSort;\n status?: FeatureStatus[];\n}\n\nexport function listKey({ sort, status }: ListQuery): string {\n return status?.length ? `${sort}:${[...status].sort().join(\",\")}` : sort;\n}\n\n/**\n * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked\n * request goes through the same vote, comment and follow updates as a row in a list.\n */\nexport function featureKey(featureId: string): string {\n return `feature:${featureId}`;\n}\n"],"mappings":"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","names":[]}
|