@fewchurs/react 1.4.0 → 1.5.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/dist/core.d.mts +5 -4
- package/dist/core.d.ts +5 -4
- package/dist/core.js +44 -24
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +43 -24
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +137 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -64
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +21 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
FewchursProvider: () => FewchursProvider,
|
|
39
39
|
FollowForm: () => FollowForm,
|
|
40
40
|
LIMITS: () => LIMITS,
|
|
41
|
+
NOT_CONFIGURED_MESSAGE: () => NOT_CONFIGURED_MESSAGE,
|
|
41
42
|
ROADMAP_STATUSES: () => ROADMAP_STATUSES,
|
|
42
43
|
SDK_HEADER: () => SDK_HEADER,
|
|
43
44
|
SDK_VERSION: () => SDK_VERSION,
|
|
@@ -85,7 +86,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
85
86
|
var import_react = require("react");
|
|
86
87
|
|
|
87
88
|
// src/client/version.ts
|
|
88
|
-
var SDK_VERSION = "1.
|
|
89
|
+
var SDK_VERSION = "1.5.0";
|
|
89
90
|
|
|
90
91
|
// src/client/constants.ts
|
|
91
92
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -103,17 +104,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
103
104
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
104
105
|
|
|
105
106
|
// src/client/errors.ts
|
|
107
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
106
108
|
var USER_MESSAGES = {
|
|
107
|
-
invalid_key: "
|
|
108
|
-
validation_failed: "
|
|
109
|
-
rate_limited: "You
|
|
110
|
-
not_found: "
|
|
111
|
-
limit_reached: "
|
|
112
|
-
server: "Something went wrong.
|
|
113
|
-
network: "
|
|
109
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
110
|
+
validation_failed: "Invalid input.",
|
|
111
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
112
|
+
not_found: "This request no longer exists.",
|
|
113
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
114
|
+
server: "Something went wrong. Try again.",
|
|
115
|
+
network: "You're offline."
|
|
114
116
|
};
|
|
117
|
+
function isWrongApp(error) {
|
|
118
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
119
|
+
}
|
|
120
|
+
function isNotConfigured(error) {
|
|
121
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
122
|
+
}
|
|
123
|
+
function notConfiguredError() {
|
|
124
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
125
|
+
}
|
|
115
126
|
function userMessage(error) {
|
|
116
127
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
128
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
117
129
|
return USER_MESSAGES[error.code];
|
|
118
130
|
}
|
|
119
131
|
function networkError(message = "Network request failed.") {
|
|
@@ -124,17 +136,17 @@ function validationError(message) {
|
|
|
124
136
|
}
|
|
125
137
|
function errorFromResponse(status, body) {
|
|
126
138
|
const code = body?.error?.code;
|
|
127
|
-
const
|
|
139
|
+
const serverMessage = body?.error?.message;
|
|
140
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
128
141
|
switch (code) {
|
|
129
142
|
case "invalid_key":
|
|
130
143
|
return { code: "invalid_key", status: 401, message };
|
|
131
144
|
case "wrong_app":
|
|
132
145
|
return { code: "invalid_key", status: 403, message };
|
|
133
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
134
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
135
146
|
case "missing_device_id":
|
|
147
|
+
return { code: "server", status: 400, message };
|
|
136
148
|
case "validation_failed":
|
|
137
|
-
return { code: "validation_failed", status: 400, message };
|
|
149
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
138
150
|
case "rate_limited":
|
|
139
151
|
return { code: "rate_limited", status: 429, message };
|
|
140
152
|
case "not_found":
|
|
@@ -228,11 +240,8 @@ function normalizeVote(value) {
|
|
|
228
240
|
// src/client/validate.ts
|
|
229
241
|
function validateFeatureInput(input) {
|
|
230
242
|
const title = input.title.trim();
|
|
231
|
-
if (title.length < LIMITS.titleMin) {
|
|
232
|
-
return validationError(`Title must be
|
|
233
|
-
}
|
|
234
|
-
if (title.length > LIMITS.titleMax) {
|
|
235
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
243
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
244
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
236
245
|
}
|
|
237
246
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
238
247
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -244,9 +253,8 @@ function validateFeatureInput(input) {
|
|
|
244
253
|
}
|
|
245
254
|
function validateCommentInput(input) {
|
|
246
255
|
const text = input.text.trim();
|
|
247
|
-
if (text.length < LIMITS.commentMin
|
|
248
|
-
|
|
249
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
256
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
257
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
250
258
|
}
|
|
251
259
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
252
260
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -266,6 +274,7 @@ function createFewchursClient(options = {}) {
|
|
|
266
274
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
267
275
|
const retries = options.retries ?? 1;
|
|
268
276
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
277
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
269
278
|
function getDeviceId() {
|
|
270
279
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
271
280
|
}
|
|
@@ -292,6 +301,11 @@ function createFewchursClient(options = {}) {
|
|
|
292
301
|
}
|
|
293
302
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
294
303
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
304
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
305
|
+
const failure = notConfiguredError();
|
|
306
|
+
logError(failure);
|
|
307
|
+
return { ok: false, error: failure };
|
|
308
|
+
}
|
|
295
309
|
const target = url(path, query);
|
|
296
310
|
const init = { method, headers: headers(body !== void 0) };
|
|
297
311
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -411,16 +425,21 @@ function delay(ms) {
|
|
|
411
425
|
function messageOf(error) {
|
|
412
426
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
413
427
|
}
|
|
414
|
-
var INVALID_KEY_HELP = "
|
|
428
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
429
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
430
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
415
431
|
var loggedErrors = /* @__PURE__ */ new Set();
|
|
416
|
-
function
|
|
432
|
+
function logOnce(detail) {
|
|
417
433
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
418
|
-
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
419
|
-
const detail = isInvalidKey ? INVALID_KEY_HELP : error.message;
|
|
420
434
|
if (loggedErrors.has(detail)) return;
|
|
421
435
|
loggedErrors.add(detail);
|
|
422
436
|
console.warn(`[fewchurs] : ${detail}`);
|
|
423
437
|
}
|
|
438
|
+
function logError(error) {
|
|
439
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
440
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
441
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
442
|
+
}
|
|
424
443
|
|
|
425
444
|
// src/identity/deviceId.ts
|
|
426
445
|
var COOKIE_NAME = "fewchurs_did";
|
|
@@ -850,11 +869,19 @@ var DEFAULT_LABELS = {
|
|
|
850
869
|
loadMore: "Load more",
|
|
851
870
|
retry: "Try again",
|
|
852
871
|
errorInvalidKeyTitle: "Invalid API key",
|
|
853
|
-
errorInvalidKeyDetail: "
|
|
854
|
-
errorWrongAppTitle: "
|
|
855
|
-
errorWrongAppDetail: "
|
|
856
|
-
errorTitle: "
|
|
857
|
-
errorDetail: "
|
|
872
|
+
errorInvalidKeyDetail: "Copy the key from your Fewchurs dashboard (Apps \u2192 API keys) and pass it to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
873
|
+
errorWrongAppTitle: "Invalid API key",
|
|
874
|
+
errorWrongAppDetail: "",
|
|
875
|
+
errorTitle: "Couldn't load requests",
|
|
876
|
+
errorDetail: "Something went wrong. Try again.",
|
|
877
|
+
errorOfflineTitle: "You're offline",
|
|
878
|
+
errorOfflineDetail: "Check your connection and try again.",
|
|
879
|
+
errorNotSetUpTitle: "Fewchurs isn't set up",
|
|
880
|
+
errorNotSetUpDetail: "Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
881
|
+
errorLoadMore: "Couldn't load more.",
|
|
882
|
+
errorComments: "Couldn't load comments.",
|
|
883
|
+
fieldTitleTooShort: "At least 3 characters.",
|
|
884
|
+
fieldEmailInvalid: "Enter a valid email address.",
|
|
858
885
|
comments: "Comments",
|
|
859
886
|
commentPlaceholder: "Add a comment",
|
|
860
887
|
postComment: "Post",
|
|
@@ -1053,6 +1080,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1053
1080
|
store.ensureComments(featureId);
|
|
1054
1081
|
}, [store, featureId, enabled, isReady]);
|
|
1055
1082
|
const loadMore = (0, import_react9.useCallback)(() => store.loadMoreComments(featureId), [store, featureId]);
|
|
1083
|
+
const refresh = (0, import_react9.useCallback)(() => store.refreshComments(featureId), [store, featureId]);
|
|
1056
1084
|
const add = (0, import_react9.useCallback)(
|
|
1057
1085
|
async (text, authorName) => {
|
|
1058
1086
|
setIsSubmitting(true);
|
|
@@ -1073,6 +1101,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1073
1101
|
submitError,
|
|
1074
1102
|
hasMore: comments.cursor !== null,
|
|
1075
1103
|
loadMore,
|
|
1104
|
+
refresh,
|
|
1076
1105
|
add
|
|
1077
1106
|
};
|
|
1078
1107
|
}
|
|
@@ -1092,10 +1121,15 @@ function formatDate(iso) {
|
|
|
1092
1121
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1093
1122
|
function CommentList({ featureId }) {
|
|
1094
1123
|
const { labels, cx } = useBoard();
|
|
1095
|
-
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
|
|
1124
|
+
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
|
|
1096
1125
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-comments"), children: [
|
|
1097
1126
|
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.loading }),
|
|
1098
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
1127
|
+
error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-empty", "fw-empty--compact"), role: "alert", children: [
|
|
1128
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__title"), children: labels.errorComments }),
|
|
1129
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__detail"), children: isNotConfigured(error) ? labels.errorNotSetUpTitle : userMessage(error) }),
|
|
1130
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1131
|
+
] }),
|
|
1132
|
+
error && comments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1099
1133
|
!isLoading && !error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.noComments }),
|
|
1100
1134
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: cx("fw-comment-list"), children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: cx("fw-comment"), children: [
|
|
1101
1135
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cx("fw-comment__author"), children: comment.authorName }),
|
|
@@ -1166,7 +1200,7 @@ function CommentForm({ featureId }) {
|
|
|
1166
1200
|
}
|
|
1167
1201
|
)
|
|
1168
1202
|
] }),
|
|
1169
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting, children: labels.postComment }),
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting || !text.trim(), children: labels.postComment }),
|
|
1170
1204
|
submitError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
|
|
1171
1205
|
]
|
|
1172
1206
|
}
|
|
@@ -1182,20 +1216,24 @@ function FollowForm({ feature }) {
|
|
|
1182
1216
|
const [email, setEmail] = (0, import_react11.useState)(userEmail ?? "");
|
|
1183
1217
|
const [isOpen, setIsOpen] = (0, import_react11.useState)(false);
|
|
1184
1218
|
if (isFollowing) {
|
|
1185
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1219
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1220
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1221
|
+
"button",
|
|
1222
|
+
{
|
|
1223
|
+
type: "button",
|
|
1224
|
+
className: cx("fw-link"),
|
|
1225
|
+
disabled: isPending,
|
|
1226
|
+
onClick: () => void unfollow(),
|
|
1227
|
+
children: labels.following
|
|
1228
|
+
}
|
|
1229
|
+
),
|
|
1230
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1231
|
+
] });
|
|
1195
1232
|
}
|
|
1196
1233
|
if (!isOpen) {
|
|
1197
1234
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
|
|
1198
1235
|
}
|
|
1236
|
+
const isEmailValid = EMAIL_PATTERN.test(email.trim());
|
|
1199
1237
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1200
1238
|
"form",
|
|
1201
1239
|
{
|
|
@@ -1219,7 +1257,8 @@ function FollowForm({ feature }) {
|
|
|
1219
1257
|
required: true
|
|
1220
1258
|
}
|
|
1221
1259
|
),
|
|
1222
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("
|
|
1260
|
+
email !== "" && !isEmailValid && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), children: labels.fieldEmailInvalid }),
|
|
1261
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !isEmailValid, children: labels.follow }),
|
|
1223
1262
|
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1224
1263
|
]
|
|
1225
1264
|
}
|
|
@@ -1262,7 +1301,7 @@ function FeatureCard({
|
|
|
1262
1301
|
defaultCommentsOpen = false
|
|
1263
1302
|
}) {
|
|
1264
1303
|
const { labels, allowComments, cx } = useBoard();
|
|
1265
|
-
const { isPending, toggle } = useVote(feature.id);
|
|
1304
|
+
const { isPending, toggle, error: voteError } = useVote(feature.id);
|
|
1266
1305
|
const [showComments, setShowComments] = (0, import_react12.useState)(defaultCommentsOpen);
|
|
1267
1306
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: cx("fw-card"), children: [
|
|
1268
1307
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
|
|
@@ -1290,6 +1329,7 @@ function FeatureCard({
|
|
|
1290
1329
|
),
|
|
1291
1330
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FollowForm, { feature })
|
|
1292
1331
|
] }),
|
|
1332
|
+
voteError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
|
|
1293
1333
|
allowComments && showComments && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { id: `fw-comments-${feature.id}`, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CommentList, { featureId: feature.id }) })
|
|
1294
1334
|
] })
|
|
1295
1335
|
] });
|
|
@@ -1331,10 +1371,25 @@ function Bar({ width, height }) {
|
|
|
1331
1371
|
// src/components/FeatureList.tsx
|
|
1332
1372
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1333
1373
|
function errorCopy(error, labels) {
|
|
1334
|
-
if (error
|
|
1335
|
-
return { title: labels.
|
|
1374
|
+
if (isNotConfigured(error)) {
|
|
1375
|
+
return { title: labels.errorNotSetUpTitle, detail: labels.errorNotSetUpDetail };
|
|
1376
|
+
}
|
|
1377
|
+
if (error.code === "network") {
|
|
1378
|
+
return { title: labels.errorOfflineTitle, detail: labels.errorOfflineDetail };
|
|
1379
|
+
}
|
|
1380
|
+
if (isWrongApp(error)) {
|
|
1381
|
+
return {
|
|
1382
|
+
title: labels.errorWrongAppTitle,
|
|
1383
|
+
detail: labels.errorWrongAppDetail || userMessage(error)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (error.code === "invalid_key") {
|
|
1387
|
+
return { title: labels.errorInvalidKeyTitle, detail: labels.errorInvalidKeyDetail };
|
|
1336
1388
|
}
|
|
1337
|
-
return
|
|
1389
|
+
return {
|
|
1390
|
+
title: labels.errorTitle,
|
|
1391
|
+
detail: error.code === "server" ? labels.errorDetail : userMessage(error)
|
|
1392
|
+
};
|
|
1338
1393
|
}
|
|
1339
1394
|
function FeatureList() {
|
|
1340
1395
|
const { initialFeatureId } = useBoard();
|
|
@@ -1345,8 +1400,11 @@ function FeatureList() {
|
|
|
1345
1400
|
}
|
|
1346
1401
|
function PinnedFeature({ featureId }) {
|
|
1347
1402
|
const { cx } = useBoard();
|
|
1348
|
-
const { feature, isLoading } = useFeature(featureId);
|
|
1403
|
+
const { feature, isLoading, error } = useFeature(featureId);
|
|
1349
1404
|
if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureListSkeleton, { rows: 1 });
|
|
1405
|
+
if (!feature && error) {
|
|
1406
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-error", "fw-pinned"), role: "alert", children: userMessage(error) });
|
|
1407
|
+
}
|
|
1350
1408
|
if (!feature) return null;
|
|
1351
1409
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pinned"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { className: cx("fw-list"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureCard, { feature, defaultCommentsOpen: true }) }) });
|
|
1352
1410
|
}
|
|
@@ -1369,7 +1427,7 @@ function Listing({ excludeId }) {
|
|
|
1369
1427
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-empty"), role: "alert", children: [
|
|
1370
1428
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__title"), children: title }),
|
|
1371
1429
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__detail"), children: detail }),
|
|
1372
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1430
|
+
!isNotConfigured(error) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1373
1431
|
] });
|
|
1374
1432
|
}
|
|
1375
1433
|
if (isLoading && visible.length === 0) {
|
|
@@ -1384,7 +1442,11 @@ function Listing({ excludeId }) {
|
|
|
1384
1442
|
}
|
|
1385
1443
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
1386
1444
|
isRoadmap ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RoadmapColumns, { features: visible }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Rows2, { features: visible }),
|
|
1387
|
-
hasMore && /* @__PURE__ */ (0, import_jsx_runtime9.
|
|
1445
|
+
hasMore && error && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-pagination"), role: "alert", children: [
|
|
1446
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.errorLoadMore }),
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void loadMore(), children: labels.retry })
|
|
1448
|
+
] }),
|
|
1449
|
+
hasMore && !error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1388
1450
|
"button",
|
|
1389
1451
|
{
|
|
1390
1452
|
type: "button",
|
|
@@ -1465,6 +1527,10 @@ function SubmitForm() {
|
|
|
1465
1527
|
const [title, setTitle] = (0, import_react14.useState)("");
|
|
1466
1528
|
const [description, setDescription] = (0, import_react14.useState)("");
|
|
1467
1529
|
const [email, setEmail] = (0, import_react14.useState)(userEmail ?? "");
|
|
1530
|
+
const isTitleValid = title.trim().length >= LIMITS.titleMin;
|
|
1531
|
+
const isEmailInvalid = emailField !== "off" && email !== "" && !EMAIL_PATTERN.test(email.trim());
|
|
1532
|
+
const isEmailMissing = emailField === "required" && !email.trim();
|
|
1533
|
+
const canSubmit = isTitleValid && !isEmailInvalid && !isEmailMissing;
|
|
1468
1534
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
1469
1535
|
"form",
|
|
1470
1536
|
{
|
|
@@ -1498,7 +1564,15 @@ function SubmitForm() {
|
|
|
1498
1564
|
onChange: (event) => setTitle(event.target.value),
|
|
1499
1565
|
required: true
|
|
1500
1566
|
}
|
|
1501
|
-
)
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1569
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cx("fw-error"), children: title !== "" && !isTitleValid ? labels.fieldTitleTooShort : "" }),
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1571
|
+
title.length,
|
|
1572
|
+
"/",
|
|
1573
|
+
LIMITS.titleMax
|
|
1574
|
+
] })
|
|
1575
|
+
] })
|
|
1502
1576
|
] }),
|
|
1503
1577
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1504
1578
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
|
|
@@ -1512,7 +1586,15 @@ function SubmitForm() {
|
|
|
1512
1586
|
placeholder: labels.fieldDescriptionPlaceholder,
|
|
1513
1587
|
onChange: (event) => setDescription(event.target.value)
|
|
1514
1588
|
}
|
|
1515
|
-
)
|
|
1589
|
+
),
|
|
1590
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1591
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", {}),
|
|
1592
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1593
|
+
description.length,
|
|
1594
|
+
"/",
|
|
1595
|
+
LIMITS.descriptionMax
|
|
1596
|
+
] })
|
|
1597
|
+
] })
|
|
1516
1598
|
] }),
|
|
1517
1599
|
emailField !== "off" && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1518
1600
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
|
|
@@ -1529,11 +1611,11 @@ function SubmitForm() {
|
|
|
1529
1611
|
required: emailField === "required"
|
|
1530
1612
|
}
|
|
1531
1613
|
),
|
|
1532
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1614
|
+
isEmailInvalid ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-error"), children: labels.fieldEmailInvalid }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1533
1615
|
] }),
|
|
1534
1616
|
error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1535
1617
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-form__actions"), children: [
|
|
1536
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: isPending ? labels.submitting : labels.submit }),
|
|
1618
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !canSubmit, children: isPending ? labels.submitting : labels.submit }),
|
|
1537
1619
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1538
1620
|
"button",
|
|
1539
1621
|
{
|
|
@@ -1846,6 +1928,7 @@ function FewchursButton({
|
|
|
1846
1928
|
FewchursProvider,
|
|
1847
1929
|
FollowForm,
|
|
1848
1930
|
LIMITS,
|
|
1931
|
+
NOT_CONFIGURED_MESSAGE,
|
|
1849
1932
|
ROADMAP_STATUSES,
|
|
1850
1933
|
SDK_HEADER,
|
|
1851
1934
|
SDK_VERSION,
|